Browse Source

da da da.

Francesco Cappelli 2 years ago
parent
commit
db12c674ef

+ 151 - 54
commands/builder.py

@@ -1,6 +1,7 @@
 from evennia import default_cmds, create_object, search_tag
 from evennia.utils import inherits_from
 from evennia.utils.eveditor import EvEditor
+from evennia.prototypes import spawner
 
 from commands.command import Command
 from typeclasses.exits import BaseDoor
@@ -24,6 +25,7 @@ def _descdoor_quit(caller):
     caller.attributes.remove("evmenu_target")
     caller.msg("Exited editor.")
 
+
 class CmdDescDoor(Command):
     """
     describe a BaseDoor in the current room.
@@ -129,6 +131,7 @@ class CmdOpen(default_cmds.CmdOpen):
             back_exit.db.return_exit = new_exit
         return new_exit
 
+
 class CmdUpdateLightState(Command):
     """
     update room light state.
@@ -160,14 +163,15 @@ class CmdUpdateLightState(Command):
         target.check_light_state()
         caller.msg("Performed update on {}!".format(target.key))
 
+
 class CmdZone(Command):
     """
     creates, deletes or lists zones
 
     Usage:
-      zone[/list||/del||/addroom] [zonename] [= room]
+      zone[/add||/del||/map] [zonename] [= room|size]
 
-    Creates a new zone.
+    Manages zones.
 
     """
 
@@ -182,73 +186,70 @@ class CmdZone(Command):
 
         caller = self.caller
 
-        if "list" in self.switches:
-            string = "";
+        if [ele for ele in ["del", "add", "map"] if(ele in self.switches)] and not self.args:
+            caller.msg(self.get_help(caller, self.cmdset))
+            return
+
+        if "del" in self.switches:
+            self.delete_zone()
+        elif "map" in self.switches:
+            self.print_map()
+        elif "add" in self.switches:
+            zone, errors = Zone.create(key=self.lhs, size=self.rhs)
+            if not errors:
+                caller.msg("Created zone |w{}|n.".format(zone.name))
+            else:
+                caller.msg("Errors creating zone:|n{}|n.".format(errors))
+        else:
+            string = ""
             zones = search_tag(key="zone", category="general")
             for zone in zones:
-                string +=  "|c{}|n ({})\n".format(zone.name, zone.dbref)
+                string += "|c{}|n ({})\n".format(zone.name, zone.dbref)
                 rooms = search_tag(key=zone.name, category="zoneId")
                 for room in rooms:
-                    string += "- {} ({})\n".format(room.name, room.dbref)
+                    string += "- {} ({}) ({},{})\n".format(room.name, room.dbref, room.db.x, room.db.y)
 
             caller.msg("Zones found: \n" + string)
-            return
 
-        if not self.args:
-            string = "Usage: zone[/list||/del||/addroom] [zonename] [= room]"
-            caller.msg(string)
+    def print_map(self):
+        caller = self.caller
+        zone = caller.search(self.args, global_search=True, exact=True)
+        if not zone:
             return
 
-        if "del" in self.switches:
-            self.delete_zone(self.args)
-        elif "addroom" in self.switches:
-            self.add_room_to_zone(self.lhs, self.rhs)
-        else:
-            zone = create_object(Zone, key=self.args)
-            caller.msg("Created zone |w{}|n.".format(zone.name))
+        map_str = ""
+        for y in range(zone.db.size):
+            for x in range(zone.db.size):
+                map_str += zone.ndb.map[x][y]['room_map_icon']
+
+            map_str += '|/'
+
+        caller.msg(map_str)
 
-    def delete_zone(self, zone_string):
+    def delete_zone(self):
         caller = self.caller
-        zone = caller.search(zone_string, global_search=True, exact=True)
+        zone = caller.search(self.args, global_search=True, exact=True)
         if not zone:
             return
         if not inherits_from(zone, Zone):
-            caller.msg("{} is not a valid zone.",format(zone.name))
+            caller.msg("{} is not a valid zone.".format(zone.name))
             return
 
         key = zone.name
         zone.delete()
         caller.msg("Zone {} deleted.".format(key))
 
-    def add_room_to_zone(self, zone_string, room_string):
-        caller = self.caller
-        zone = caller.search(zone_string, global_search=True, exact=True)
-        if not zone:
-            return
-        if not inherits_from(zone, Zone):
-            caller.msg("{} is not a valid zone.",format(zone.name))
-            return
-
-        room = caller.search(room_string, global_search=True)
-        if not room:
-            return
-        if not inherits_from(room, Room):
-            caller.msg("{} is not a valid room.",format(room.name))
-            return
-
-        zone.add_room(room)
-        caller.msg("{} added to zone {}.".format(room.name, zone.name))
 
 class CmdAddToZone(Command):
     """
     add a room to a zone
 
     Usage:
-        @addtozone obj = zone
+        addtozone obj = zone,x,y
 
     Adds a room to an existing zone.
     """
-    key = "@addtozone"
+    key = "addtozone"
     locks = "cmd:perm(zone) or perm(Builders)"
     help_category = "Building"
 
@@ -258,28 +259,124 @@ class CmdAddToZone(Command):
         """
 
         caller = self.caller
+        if len(self.rhslist) < 3:
+            caller.msg(self.get_help(caller, self.cmdset))
+            return
 
-        if self.rhs:
-            # We have an =
-            zone = caller.search(self.rhs, global_search=True, exact=True)
+        zone_key, x, y = self.rhslist
+
+        if zone_key and x and y:
+            zone = caller.search(zone_key, global_search=True, exact=True)
             if not zone:
-                self.msg("Zone %s doesn't exist." % self.rhs)
+                caller.msg("Zone {} doesn't exist.".format(zone_key))
                 return
-            if not utils.inherits_from(zone, Zone):
-                self.msg("{r%s is not a valid zone.{n" % zone.name)
+            if not inherits_from(zone, Zone):
+                caller.msg("|r{} is not a valid zone.|n".format(zone.name))
                 return
 
-            room = caller.search(self.lhs)
+            room = caller.search(self.lhs, global_search=True, nofound_string="|rRoom {} doesn't exist.|n".format(self.lhs))
             if not room:
-                self.msg("{rRoom %s doesn't exist.{n" % self.lhs)
                 return
-            if not utils.inherits_from(room, BaseRoom):
-                self.msg("{r%s is not a valid room.{n" % room.name)
+            if not inherits_from(room, Room):
+                caller.msg("|r{} is not a valid room.|n".format(room.name))
+                return
+            try:
+                if room.db.zone:
+                    caller.msg("|r{} is already assigned to zone {}.|n".format(room.name, zone.name))
+                    return
+                zone.add_room(room, x, y)
+            except ValueError as ve:
+                caller.msg(ve.args[0])
                 return
 
-            room.add_to_zone(zone.name)
-            self.msg("Room %s (%s) added to zone %s (%s)." % (room.name, room.dbref, zone.name, zone.dbref))
-
+            caller.msg("Room {} ({}) added to zone {} ({}).".format(room.name, room.dbref, zone.name, zone.dbref))
         else:
-            self.msg("{rUsage: @addtozone obj = zone{n")
+            caller.msg(self.get_help(caller, self.cmdset))
+            return
+
+
+class CmdPopen(Command):
+    """
+    open a new exit from prototype linking two rooms
+
+    Usage:
+      popen <prototype>[;alias;alias..] [,<return exit>[;alias;..]]] = <origin>,<destination>
+
+    Handles the creation of exits. If a destination is given, the exit
+    will point there. The <return exit> argument sets up an exit at the
+    destination leading back to the current room. Destination name
+    can be given both as a #dbref and a name, if that name is globally
+    unique.
+
+    """
+    key = "popen"
+    locks = "cmd:perm(open) or perm(Builder)"
+    help_category = "Building"
+
+    new_obj_lockstring = "control:id({id}) or perm(Admin);delete:id({id}) or perm(Admin)"
+
+    def func(self):
+        """
+        This is where the processing starts.
+        Uses the ObjManipCommand.parser() for pre-processing
+        as well as the self.create_exit() method.
+        """
+        caller = self.caller
+
+        if not self.args or not self.rhs or len(self.rhslist) != 2:
+            caller.msg(self.get_help(caller, self.cmdset))
+            return
+
+        exit_prototype = self.lhs_objs[0]["name"]
+        exit_aliases = self.lhs_objs[0]["aliases"]
+
+        location_name, destination_name = self.rhslist
+
+        # first, check if the destination and origin exist.
+        destination = caller.search(destination_name, global_search=True)
+        if not destination:
+            return
+
+        location = caller.search(location_name, global_search=True)
+        if not location:
+            return
+
+        try:
+            exit_obj, *rest = spawner.spawn(exit_prototype)
+        except KeyError:
+            caller.msg("Prototype {} not found".format(exit_prototype))
             return
+
+        exit_obj.location = location
+        exit_obj.destination = destination
+        exit_obj.aliases.add(exit_aliases)
+
+        return_exit_object = None
+
+        if len(self.lhs_objs) == 2:
+            return_exit_prototype = self.lhs_objs[1]["name"]
+            return_exit_aliases = self.lhs_objs[1]["aliases"]
+
+            try:
+                return_exit_object, *rest = spawner.spawn(return_exit_prototype)
+            except KeyError:
+                caller.msg("Return prototype {} not found, rolling back...".format(return_exit_prototype))
+                exit_obj.delete()
+                return
+
+            return_exit_object.location = destination
+            return_exit_object.destination = location
+
+        # BaseDoor requires a return exit
+        if inherits_from(exit_obj, "typeclasses.exits.BaseDoor"):
+            if not return_exit_object:
+                return_exit_object, *rest = spawner.spawn(exit_prototype)
+                return_exit_object.location = destination
+                return_exit_object.destination = location
+
+            exit_obj.db.return_exit = return_exit_object
+            return_exit_object.db.return_exit = exit_obj
+
+        caller.msg("Created exit {} from {} to {}.".format(exit_obj.name, location.name, destination.name))
+        if return_exit_object:
+            caller.msg("Created exit {} from {} to {}.".format(return_exit_object.name, destination.name, location.name))

+ 51 - 9
commands/command.py

@@ -14,7 +14,7 @@ from evennia.utils import inherits_from
 from evennia.utils.utils import list_to_string
 from evennia.utils.eveditor import EvEditor
 
-from utils.building import create_room, create_exit
+from utils.building import create_exit
 from utils.utils import has_tag, fmt_light, fmt_dark, toggle_effect, has_effect, indefinite_article
 from typeclasses.exits import BaseDoor
 from typeclasses.rooms import IndoorRoom
@@ -47,6 +47,39 @@ class Command(default_cmds.MuxCommand):
 
     """
 
+    def parse(self):
+        """
+        We need to expand the default parsing to get all
+        the cases, see the module doc.
+        """
+        # get all the normal parsing done (switches etc)
+        super().parse()
+
+        obj_defs = ([], [])  # stores left- and right-hand side of '='
+        obj_attrs = ([], [])  # "
+
+        for iside, arglist in enumerate((self.lhslist, self.rhslist)):
+            # lhslist/rhslist is already split by ',' at this point
+            for objdef in arglist:
+                aliases, option, attrs = [], None, []
+                if ":" in objdef:
+                    objdef, option = [part.strip() for part in objdef.rsplit(":", 1)]
+                if ";" in objdef:
+                    objdef, aliases = [part.strip() for part in objdef.split(";", 1)]
+                    aliases = [alias.strip() for alias in aliases.split(";") if alias.strip()]
+                if "/" in objdef:
+                    objdef, attrs = [part.strip() for part in objdef.split("/", 1)]
+                    attrs = [part.strip().lower() for part in attrs.split("/") if part.strip()]
+                # store data
+                obj_defs[iside].append({"name": objdef, "option": option, "aliases": aliases})
+                obj_attrs[iside].append({"name": objdef, "attrs": attrs})
+
+        # store for future access
+        self.lhs_objs = obj_defs[0]
+        self.rhs_objs = obj_defs[1]
+        self.lhs_objattr = obj_attrs[0]
+        self.rhs_objattr = obj_attrs[1]
+
     def at_post_cmd(self):
         caller = self.caller
         prompt = "|_|/°|w%s|n°: " % (caller.location)
@@ -103,7 +136,7 @@ class CmdGet(Command):
             caller.msg("Get what?")
             return
 
-        if inherits_from(caller.location, IndoorRoom) and not caller.location.db.is_lit:
+        if inherits_from(caller.location, IndoorRoom) and not caller.location.db.is_lit and not caller.is_superuser:
             caller.msg("Its too dark to get anything.")
             return
 
@@ -543,7 +576,12 @@ class CmdInventory(Command):
 
 class CmdCast(Command):
     """
+    cast a spell.
 
+    Usage:
+      cast <spell> [at <target>]
+
+    Casts a spell.
     """
     key = "cast"
     aliases = ["cs"]
@@ -555,16 +593,18 @@ class CmdCast(Command):
         """
         Handle parsing of:
         ::
-            <spell> [at <target>]
+            <spell> [at|on <target>]
         """
         self.args = args = self.args.strip().lower()
-        spell_name, target_name = "", ""
 
         if " at " in args:
             spell_name, *rest = args.split(" at ", 1)
-            target_name = rest[0] if rest else ""
+        elif " on " in args:
+            spell_name, *rest = args.split(" on ", 1)
         else:
-            spell_name = args
+            spell_name, rest = args, []
+
+        target_name = rest[0] if rest else ""
 
         self.spell_name = spell_name.strip()
         self.target_name = target_name.strip()
@@ -573,7 +613,7 @@ class CmdCast(Command):
         caller = self.caller
 
         if not self.args or not self.spell_name:
-            caller.msg("Usage: cast <spell> [at <target>]")
+            caller.msg(self.get_help(caller, self.cmdset))
             return
 
         spell_id = self.spell_name.replace(' ', '_')
@@ -600,14 +640,16 @@ class CmdTestPy(Command):
     def func(self):
         caller = self.caller
 
+        caller.msg(self.lhs_objs)
+
         pattern = re.compile(r'''((?:[^ "']|"[^"]*"|'[^']*')+)''')
         self.args = pattern.split(self.lhs)[1::2]
 
         # room = create_room(self.args[0].strip(" \"'"),  int(self.args[1].strip(" \"'")), int(self.args[2].strip(" \"'")), self.args[3].strip(" \"'"))
         # caller.msg(room)
 
-        exit = create_exit("exit_empty", caller.location, "north")
-        caller.msg(exit)
+        # exit = create_exit("exit_empty", caller.location, "north")
+        # caller.msg(exit)
 
         # caller.msg(dkmud_oob=({"testarg": "valuetestarg"}))
 

+ 5 - 0
commands/default_cmdsets.py

@@ -32,6 +32,8 @@ from commands.builder import CmdUpdateLightState
 from commands.builder import CmdOpen
 from commands.builder import CmdDescDoor
 from commands.builder import CmdZone
+from commands.builder import CmdAddToZone
+from commands.builder import CmdPopen
 
 from utils.crafting import CmdCraft
 
@@ -70,9 +72,12 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet):
         self.add(CmdOpen())
         self.add(CmdDescDoor())
         self.add(CmdZone())
+        self.add(CmdAddToZone())
+        self.add(CmdPopen())
 
         self.add(CmdCraft())
 
+
 class AccountCmdSet(default_cmds.AccountCmdSet):
     """
     This is the cmdset available to the Account at all times. It is

+ 1 - 0
server/conf/settings.py

@@ -39,6 +39,7 @@ GLOBAL_SCRIPTS = {
              'repeats': 0, 'interval': 1}
 }
 
+PROTOTYPE_MODULES += ["world.tutorial_prototypes"]
 CRAFT_RECIPE_MODULES = ['world.recipes_base']
 
 ######################################################################

+ 0 - 2
typeclasses/mobs.py

@@ -36,8 +36,6 @@ class Mob(Object):
         pass
 
     def think(self):
-
-
         if not self.db.action:
             self.db.action = create_object(ActionIdle, key="action_idle")
             self.db.action.prepare(self)

+ 64 - 23
typeclasses/rooms.py

@@ -36,6 +36,8 @@ class Room(DefaultRoom):
         self.db.x = 0
         self.db.y = 0
 
+        self.db.map_icon = '|w⊡|n'
+
         self.db.zone = None
 
 
@@ -54,6 +56,8 @@ class IndoorRoom(Room):
         "You can't see anything, but the air is damp. It feels like you are far underground.",
     )
 
+    map_icon = '|w□|n'
+
     def at_object_creation(self):
         super().at_object_creation()
         self.locks.add("search:all()")
@@ -228,9 +232,28 @@ class Zone(DefaultRoom):
         provide path-finding capabilities to mob.
     """
 
+    @classmethod
+    def create(cls, key, account=None, **kwargs):
+        description = kwargs.pop("description", "This is a zone.")
+
+        size = kwargs.pop("size")
+        size = MAP_SIZE if not size else size
+
+        zone, errors = super(Zone, cls).create(key, account=account, description=description, **kwargs)
+
+        if not errors:
+            try:
+                zone.db.size = int(size)
+            except ValueError:
+                errors.append("Size must be an integer.")
+
+        return zone, errors
+
     def at_object_creation(self):
         super().at_object_creation()
 
+        self.db.size = MAP_SIZE
+
         self.tags.add("zone", category="general")
         self.locks.add(";".join(["get:false()", "puppet:false()", "view:perm(zone) or perm(Builder)"]))
 
@@ -239,21 +262,53 @@ class Zone(DefaultRoom):
     def at_init(self):
         super().at_init()
         # when reloaded recalculate path-finding data
-        self.create_paths()
+        self._create_paths()
+
+    def add_room(self, room, x, y):
+        xi = int(x)
+        yi = int(y)
+
+        if xi < 0 or xi >= self.db.size or yi < 0 or yi >= self.db.size:
+            raise ValueError("Coordinates must be between 0 and {}.".format(self.db.size - 1))
 
-    def create_paths(self):
+        if self.ndb.map[xi][yi]['room_id'] != -1:
+            raise ValueError("Coordinates ({},{}) are not empty.".format(x, y))
+
+        room.db.x = xi
+        room.db.y = yi
+
+        room.db.zone = self.dbref
+        self._add_room_to_graph(room)
+
+    def remove_room_exit(self, exit_obj):
+        if self.ndb.sp_graph.is_vertex(exit_obj.location) and self.ndb.sp_graph.is_edge(exit_obj.location,
+                                                                                        exit_obj.destination):
+            self.ndb.sp_graph.del_edge(exit_obj.location, exit_obj.destination)
+
+    def delete(self):
+        rooms = search_tag(key=self.name, category="zoneId")
+        for room in rooms:
+            room.tags.remove(self.name, category="zoneId")
+
+        return super().delete()
+
+    def shortest_path(self, start, end):
+        return spath.shortestPath(self.ndb.sp_graph, start, end)
+
+    def _create_paths(self):
         self.ndb.sp_graph = spath.Graph()
-        self.ndb.map = [[{"room_id": -1} for i in range(MAP_SIZE)] for j in range(MAP_SIZE)]
+        self.ndb.map = [[{'room_id': -1, 'room_map_icon': "|=g∙|n"} for i in range(self.db.size)] for j in range(self.db.size)]
 
         rooms = search_tag(key=self.name, category="zoneId")
 
         for room in rooms:
-            self.add_room(room)
+            self._add_room_to_graph(room)
 
-    def add_room(self, room):
+    def _add_room_to_graph(self, room):
         # add to map
-        if 0 <= room.db.x < MAP_SIZE and 0 <= room.db.y < MAP_SIZE and self.ndb.map[room.db.x][room.db.y]["room_id"] == -1:
-            self.ndb.map[room.db.x][room.db.y]["room_id"] = room.dbref
+        if 0 <= room.db.x < self.db.size and 0 <= room.db.y < self.db.size and self.ndb.map[room.db.x][room.db.y]["room_id"] == -1:
+            self.ndb.map[room.db.x][room.db.y]['room_id'] = room.dbref
+            self.ndb.map[room.db.x][room.db.y]['room_map_icon'] = room.db.map_icon
         else:
             logger.log_err("Cannot add room {} to {} at position {}:{}.".format(room.dbref, self.dbref, room.db.x, room.db.y))
             raise Exception("Cannot add room {} to {} at position {}:{}.".format(room.dbref, self.dbref, room.db.x, room.db.y))
@@ -264,24 +319,10 @@ class Zone(DefaultRoom):
             room.tags.add(self.name, category="zoneId")
             room.db.zone = self
 
-        self.update_room_exits(room)
+        self._update_room_exits(room)
 
-    def update_room_exits(self, room):
+    def _update_room_exits(self, room):
         if self.ndb.sp_graph.is_vertex(room):
             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)
-
-    def remove_room_exit(self, exit_obj):
-        if self.ndb.sp_graph.is_vertex(exit_obj.location) and self.ndb.sp_graph.is_edge(exit_obj.location, exit_obj.destination):
-            self.ndb.sp_graph.del_edge(exit_obj.location, exit_obj.destination)
-
-    def delete(self):
-        rooms = search_tag(key=self.name, category="zoneId")
-        for room in rooms:
-            room.tags.remove(self.name, category="zoneId")
-
-        return super().delete()
-
-    def shortest_path(self, start, end):
-        return spath.shortestPath(self.ndb.sp_graph, start, end)

+ 0 - 15
utils/building.py

@@ -1,22 +1,7 @@
-from evennia.contrib.ingame_python import typeclasses
 from evennia.prototypes import spawner
 from evennia.utils import inherits_from
 from evennia.utils.search import search_object
 
-def create_room(room_prototype, x, y, zone_id):
-    zones = search_object(zone_id, typeclass="typeclasses.rooms.Zone", exact=True)
-    if not zones:
-        raise Exception("create_room: cannot find zone {}".format(zone_id))
-
-    zone = zones[0]
-    room, *rest = spawner.spawn(room_prototype)
-    room.db.x = x
-    room.db.y = y
-
-    zone.add_room(room)
-
-    return room
-
 
 def create_exit(exit_prototype, location, direction):
     x = location.db.x

+ 71 - 6
utils/crafting.py

@@ -120,6 +120,7 @@ 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.commands.cmdset import CmdSet
 from evennia.commands.command import Command
@@ -232,7 +233,7 @@ class CraftingRecipeBase:
             **kwargs: Any optional properties relevant to this send.
 
         """
-        self.crafter.msg(message, {"type": "crafting"})
+        self.crafter.msg(message, oob=({"type": "crafting"}))
 
     def pre_craft(self, **kwargs):
         """
@@ -382,6 +383,7 @@ class CraftingRecipe(CraftingRecipeBase):
     ## Properties on the class level:
 
     - `name` (str): The name of this recipe. This should be globally unique.
+    - 'crafting_time' (int): The time needed for crafting.
 
     ### tools
 
@@ -540,7 +542,9 @@ class CraftingRecipe(CraftingRecipeBase):
     # general craft-failure msg to show after other error-messages.
     failure_message = ""
     # show after a successful craft
-    success_message = "You successfully craft {outputs}!"
+    success_message = "You craft {outputs}."
+    # recipe crafting time
+    crafting_time = 1
 
     def __init__(self, crafter, *inputs, **kwargs):
         """
@@ -921,6 +925,62 @@ def craft(crafter, recipe_name, *inputs, raise_exception=False, **kwargs):
     return recipe.craft(raise_exception=raise_exception)
 
 
+def can_craft(crafter, recipe_name, *inputs, **kwargs):
+    """
+        Access function.Check if crafter can craft a given recipe from a source recipe module.
+
+        Args:
+            crafter (Object): The one doing the crafting.
+            recipe_name (str): The `CraftRecipe.name` to use. This uses fuzzy-matching
+                if the result is unique.
+            *inputs: Suitable ingredients and/or tools (Objects) to use in the crafting.
+            raise_exception (bool, optional): If crafting failed for whatever
+                reason, raise `CraftingError`. The user will still be informed by the
+                recipe.
+            **kwargs: Optional kwargs to pass into the recipe (will passed into
+                recipe.craft).
+
+        Returns:
+            list: Error messages, if any.
+
+        Raises:
+            CraftingError: If `raise_exception` is True and crafting failed to
+            produce an output.  KeyError: If `recipe_name` failed to find a
+            matching recipe class (or the hit was not precise enough.)
+
+        Notes:
+            If no recipe_module is given, will look for a list `settings.CRAFT_RECIPE_MODULES` and
+            lastly fall back to the example module `"evennia.contrib."`
+
+        """
+    # delayed loading/caching of recipes
+    _load_recipes()
+
+    RecipeClass = search_recipe(crafter, recipe_name)
+
+    if not RecipeClass:
+        raise KeyError(
+            f"No recipe in settings.CRAFT_RECIPE_MODULES has a name matching {recipe_name}"
+        )
+    recipe = RecipeClass(crafter, *inputs, **kwargs)
+
+    if recipe.allow_craft:
+
+        # override/extend craft_kwargs from initialization.
+        craft_kwargs = copy(recipe.craft_kwargs)
+        craft_kwargs.update(kwargs)
+
+        try:
+            recipe.pre_craft(**craft_kwargs)
+        except (CraftingError, CraftingValidationError):
+            logger.log_err(CraftingValidationError.args)
+            return False
+        else:
+            return True
+
+    return False
+
+
 def search_recipe(crafter, recipe_name):
     # delayed loading/caching of recipes
     _load_recipes()
@@ -933,7 +993,7 @@ def search_recipe(crafter, recipe_name):
             # try in-match
             matches = [key for key in _RECIPE_CLASSES if recipe_name in key]
         if len(matches) == 1:
-            recipe_class = matches[0]
+            recipe_class = _RECIPE_CLASSES.get(matches[0], None)
 
     return recipe_class
 
@@ -1068,13 +1128,18 @@ class CmdCraft(Command):
                 return
             tools.append(obj)
 
-        if not search_recipe(caller, self.recipe):
+        recipe_cls = search_recipe(caller, self.recipe)
+        if not recipe_cls:
             caller.msg("You don't know how to craft {} {}.".format(indefinite_article(self.recipe), self.recipe))
             return
 
+        tools_and_ingredients = tools + ingredients
+        if not can_craft(caller, recipe_cls.name, *tools_and_ingredients):
+            return
+
         toggle_effect(caller, "is_busy")
-        caller.msg("You start crafting {} {}.".format(indefinite_article(self.recipe), self.recipe))
-        action_script = create_script("utils.crafting.CmdCraftComplete", obj=caller, interval=15, attributes=[("recipe", self.recipe), ("tools_and_ingredients", tools + ingredients)])
+        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)])
         caller.db.current_action = action_script
 
 

+ 0 - 1
utils/utils.py

@@ -1,6 +1,5 @@
 from evennia.prototypes import spawner
 
-
 def has_tag(obj, key, category):
     return obj.tags.get(key=key, category=category) != None;
 

+ 10 - 7
world/batches/init.ev

@@ -2,6 +2,9 @@
 # -must- be separated by at least one comment-line.
 @tel #2
 #
+zone/add tutorial_zone = 32
+#
+
 @dig/tel ruined room;start_00:typeclasses.rooms.IndoorRoom
 #
 @desc here =
@@ -23,6 +26,7 @@ clutched to the handle of a broken spear.
 #
 @lock skeleton = search:all()
 #
+
 @dig/tel long hall;hall;start_01:typeclasses.rooms.IndoorRoom
 #
 @desc start_01 =
@@ -59,6 +63,7 @@ filling the floor with debris.
 #delle assi portanti. Una parte delle pietre di copertura sono rovinate al suolo,
 #riempiendo il pavimento di detriti.
 #
+
 @dig/tel old guardhouse;guardhouse;start_02:typeclasses.rooms.IndoorRoom
 #
 @desc start_02 =
@@ -99,17 +104,15 @@ A big oak door, reinforced with iron bars across its frame.
 It bears marks and burns all over its surface but hasn't been breached during the
 siege.
 #
-zone tutorial_zone
-#
-zone/addroom tutorial_zone = start_door_00
+addtozone start_00 = tutorial_zone, 16, 30
 #
-zone/addroom tutorial_zone = start_door_01
+addtozone start_01 = tutorial_zone, 16, 29
 #
-zone/addroom tutorial_zone = start_door_02
+addtozone start_02 = tutorial_zone, 15, 29
 #
-zone/addroom tutorial_zone = start_door_03
+addtozone start_03 = tutorial_zone, 17, 29
 #
-zone/addroom tutorial_zone = start_door_04
+addtozone start_04 = tutorial_zone, 18, 29
 #
 @tel start_00
 #

+ 45 - 0
world/batches/tutorial.ev

@@ -0,0 +1,45 @@
+# We start from limbo. Remember that every command in the batchfile
+# -must- be separated by at least one comment-line.
+@tel #2
+#
+zone/add tutorial_zone = 32
+#
+# rooms
+spawn/noloc start_00
+#
+spawn/noloc start_01
+#
+spawn/noloc start_02
+#
+spawn/noloc start_03
+#
+spawn/noloc start_04
+#
+# exits
+popen start_door_00 = start_00,start_01
+#
+popen start_door_01 = start_01,start_02
+#
+popen start_door_02 = start_01,start_03
+#
+popen start_door_03 = start_03,start_04
+#
+#add rooms to starting zone
+addtozone start_00 = tutorial_zone, 16, 30
+#
+addtozone start_01 = tutorial_zone, 16, 29
+#
+addtozone start_02 = tutorial_zone, 15, 29
+#
+addtozone start_03 = tutorial_zone, 17, 29
+#
+addtozone start_04 = tutorial_zone, 18, 29
+#
+tel start_00
+#
+spawn f_armored_skeleton
+#
+tel start_01
+#
+spawn f_rubble_01
+#

+ 133 - 56
world/prototypes.py

@@ -48,133 +48,210 @@ See the `spawn` command and `evennia.prototypes.spawner.spawn` for more info.
 
 """
 
-ROOM_EMPTY = {
-    "prototype_key": "room_empty",
+# TEMPLATES
+
+ROOM = {
+    "prototype_key": "room",
+    "prototype_tags": ["room"],
     "key": "empty room",
     "desc": "An empty room.",
+    "map_icon": "|w□|n",
     "typeclass": "typeclasses.rooms.IndoorRoom"
 }
 
-EXIT_EMPTY = {
-    "prototype_key": "exit_empty",
+EXIT = {
+    "prototype_key": "exit",
+    "prototype_tags": ["room", "exit"],
     "key": "corridor",
     "desc": "An empty corridor.",
     "typeclass": "typeclasses.exits.BaseDoor"
 }
 
-BROKEN_CROWN = {
-    "prototype_key": "broken_crown",
-    "key": "broken crown",
-    "desc": "An old iron crown, dented and covered in rust.",
-    "typeclass": "typeclasses.objects.EquippableItem",
-    "slot": 'head'
-}
-
-
-MULTICOLORED_ROBE = {
-    "prototype_key": "multicolored robe",
-    "key": "multicolored robe",
-    "desc": "A long robe, made of many different colored cloth patches.",
-    "typeclass": "typeclasses.objects.EquippableItem",
-    "slot": 'torso'
+ITEM = {
+    "prototype_key": "item",
+    "prototype_tags": ["item"],
+    "key": "item",
+    "desc": "An unremarkable item made of dreams.",
+    "typeclass": "typeclasses.objects.Item"
 }
 
-PLAIN_TROUSERS = {
-    "prototype_key": "plain trousers",
-    "key": "plain trousers",
-    "desc": "Simple but robust cloth trousers.",
+ITEM_EQUIPPABLE = {
+    "prototype_key": "item_equippable",
+    "prototype_tags": ["item", "equippable"],
+    "key": "equippable item",
+    "desc": "An unremarkable equippable item made of dreams.",
     "typeclass": "typeclasses.objects.EquippableItem",
-    "slot": 'legs'
+    "slot": 'head'
 }
 
-LEATHER_BOOTS = {
-    "prototype_key": "leather boots",
-    "key": "leather boots",
-    "desc": "A worn pair of leather boots.",
-    "typeclass": "typeclasses.objects.EquippableItem",
-    "slot": 'foot'
+FEATURE = {
+    "prototype_key": "feature",
+    "prototype_tags": ["feature"],
+    "key": "feature",
+    "desc": "Something slightly remarkable.",
+    "feature_desc": "A slightly remarkable |wfeature|n.",
+    "typeclass": "typeclasses.objects.Feature"
 }
 
 FEATURE_CONTAINER = {
     "prototype_key": "feature_container",
-    "key": "chest",
-    "desc": "A chest.",
-    "feature_desc": "A |wchest|n lies on the floor.",
+    "prototype_tags": ["feature", "container"],
+    "key": "generic container",
+    "desc": "A generic container.",
+    "feature_desc": "A dreadful |wgeneric container|n lies on the floor.",
     "typeclass": "typeclasses.objects.ContainerFeature"
 }
 
+# FEATURES
+
+F_ARMORED_SKELETON = {
+    "prototype_parent": "FEATURE",
+    "prototype_tags": ["feature"],
+    "prototype_key": "f_armored_skeleton",
+    "key": "skeleton of a soldier in armor",
+    "aliases": ["skeleton"],
+    "desc": "The skeleton of a soldier, still locked in their armor now "
+            "rusty. They lie leaning against the barricade where he died, their bony hand "
+            "clutched to the handle of a broken spear.",
+    "feature_desc": "A |wskeleton|n in a broken armor is collapsed on the floor behind the table.",
+    "locks": "search:all()"
+}
+
+F_RUBBLE_01 = {
+    "prototype_parent": "FEATURE",
+    "prototype_tags": ["feature"],
+    "prototype_key": "f_rubble_01",
+    "key": "pile of stones",
+    "aliases": ["pile"],
+    "desc": "A large root system pierced the ceiling of this room, shattering one"
+            " of the load-bearing boards. Some of the covering stones now lie damaged on the ground,"
+            "filling the floor with debris.",
+    "feature_desc": "A |wpile of stones|n and a collapsed beam from the ceiling make it difficult to cross this area.",
+    "locks": "search:all()"
+}
+
 FEATURE_SKELETON = {
+    "prototype_parent": "FEATURE",
+    "prototype_tags": ["feature"],
     "prototype_key": "feature_skeleton",
     "key": "rugged skeleton",
+    "aliases": ["skeleton"],
     "desc": "An old humanoid skeleton, eroded by the passage of time.",
-    "feature_desc": "A rugged humanoid |wskeleton|n lies on the floor, theirs bony hand still clutching a broken spear. What remains of theirs armor and clothings is too battered to let you recognize their origins.",
-    "typeclass": "typeclasses.objects.Feature"
+    "feature_desc": "A rugged humanoid |wskeleton|n lies on the floor, theirs bony hand still clutching a broken "
+                    "spear. What remains of theirs armor and clothing is too battered to let you recognize their "
+                    "origins.",
+    "locks": "search:all()"
 }
 
+SUMMONING_CIRCLE = {
+    "prototype_parent": "feature",
+    "prototype_tags": ["feature"],
+    "prototype_key": "summoning_circle",
+    "key": "circle of summoning",
+    "aliases": ["circle", "summoning circle"],
+    "desc": "A circular pattern of mystical runes drawn with blood.",
+    "feature_desc": "An arcane |wcircle of summoning|n is draw with blood on the floor.",
+}
+
+# ITEMS
+
 STONE = {
+    "prototype_parent": "ITEM",
+    "prototype_tags": ["item"],
     "prototype_key": "stone",
     "key": "stone",
-    "desc": "An unremarkable stone made of granite.",
     "aliases": ["granite stone"],
-    "typeclass": "typeclasses.objects.Item"
+    "desc": "An unremarkable stone made of granite."
 }
 
 BIG_STONE = {
+    "prototype_parent": "ITEM",
+    "prototype_tags": ["item"],
     "prototype_key": "big stone",
     "key": "big stone",
-    "desc": "An unremarkable stone made of granite. It seems very heavy.",
     "aliases": ["big granite stone"],
+    "desc": "An unremarkable stone made of granite. It seems very heavy.",
     "get_err_msg": "You are not strong enough to lift this stone.",
     "locks": "get:attr_gt(strength, 50)",
-    "typeclass": "typeclasses.objects.Item"
+}
+
+BROKEN_CROWN = {
+    "prototype_parent": "ITEM_EQUIPPABLE",
+    "prototype_tags": ["item", "equippable"],
+    "prototype_key": "broken_crown",
+    "key": "broken crown",
+    "desc": "An old iron crown, dented and covered in rust.",
+    "slot": 'head'
+}
+
+MULTICOLORED_ROBE = {
+    "prototype_parent": "ITEM_EQUIPPABLE",
+    "prototype_tags": ["item", "equippable"],
+    "prototype_key": "multicolored robe",
+    "key": "multicolored robe",
+    "desc": "A long robe, made of many different colored cloth patches.",
+    "slot": 'torso'
+}
+
+PLAIN_TROUSERS = {
+    "prototype_parent": "ITEM_EQUIPPABLE",
+    "prototype_tags": ["item", "equippable"],
+    "prototype_key": "plain trousers",
+    "key": "plain trousers",
+    "desc": "Simple but robust cloth trousers.",
+    "slot": 'legs'
+}
+
+LEATHER_BOOTS = {
+    "prototype_parent": "ITEM_EQUIPPABLE",
+    "prototype_tags": ["item", "equippable"],
+    "prototype_key": "leather boots",
+    "key": "leather boots",
+    "desc": "A worn pair of leather boots.",
+    "slot": 'foot'
 }
 
 LANTERN = {
+    "prototype_parent": "ITEM",
+    "prototype_tags": ["item"],
     "prototype_key": "lantern",
     "key": "old lantern",
-    "desc": "An old lantern, still filled with oil.",
     "aliases": ["lantern"],
+    "desc": "An old lantern, still filled with oil.",
     "attrs": [("is_lit", True, None, None)],
     "tags": [("emit_light", "effect", None)],
     "locks": "light:all()",
-    "typeclass": "typeclasses.objects.Item"
 }
 
 BLADE_TOOL = {
+    "prototype_parent": "ITEM_EQUIPPABLE",
+    "prototype_tags": ["item", "equippable"],
     "prototype_key": "blade tool",
     "key": "steel blade",
-    "desc": "A steel blade, with an oak handle wrapped in cloth.",
     "aliases": ["blade"],
+    "desc": "A steel blade, with an oak handle wrapped in cloth.",
     "tags": [("blade", "crafting_tool", None)],
-    "typeclass": "typeclasses.objects.EquippableItem",
     "slot": 'foot'
 }
 
 WOOD_MATERIAL = {
+    "prototype_parent": "ITEM",
+    "prototype_tags": ["item"],
     "prototype_key": "wood_material",
     "key": "piece of wood",
-    "desc": "An unremarkable piece of wood.",
     "aliases": ["wood"],
+    "desc": "An unremarkable piece of wood.",
     "tags": [("wood", "crafting_material", None)],
-    "typeclass": "typeclasses.objects.Item"
 }
 
 BLOOD_MATERIAL = {
+    "prototype_parent": "ITEM",
+    "prototype_tags": ["item"],
     "prototype_key": "blood_material",
     "key": "vial of blood",
-    "desc": "A vial of blood. Fresh.",
     "aliases": ["blood, vial"],
+    "desc": "A vial of blood. Fresh.",
     "tags": [("blood", "crafting_material", None)],
-    "typeclass": "typeclasses.objects.Item"
-}
-
-SUMMONING_CIRCLE = {
-    "prototype_key": "summoning_circle",
-    "key": "summoning circle",
-    "aliases": ["circle"],
-    "desc": "A circular pattern of mystical runes drawn with blood.",
-    "feature_desc": "An arcane |wcircle of summoning|n is draw with blood on the floor.",
-    "typeclass": "typeclasses.objects.Feature"
 }
 
 ## example of module-based prototypes using

+ 30 - 7
world/recipes_base.py

@@ -1,23 +1,46 @@
 from utils.crafting import CraftingRecipe
 
 
-class WoodenPuppetRecipe(CraftingRecipe):
-    """A puppet"""
-    name = "wooden puppet"  # name to refer to this recipe as
+class BloodRecipe(CraftingRecipe):
+    """Some blood"""
+    name = "vial of blood"  # name to refer to this recipe as
+    crafting_time = 5
     tool_tags = ["blade"]
-    consumable_tags = ["wood"]
+    consumable_tags = []
     output_prototypes = [
-        {"key": "carved wooden doll",
-         "typeclass": "typeclasses.objects.Item",
-         "desc": "A small carved doll"}
+        "blood_material"
     ]
+    output_names = ["a vial of blood"]
+    success_message = "You collect your blood in a vial."
+
+    def post_craft(self, craft_result, **kwargs):
+        result_obj = super().post_craft(craft_result, **kwargs)
+        if result_obj and self.crafter.attributes.has('health'):
+            self.crafter.db.health -= 1
+
+        return result_obj
 
 
 class SummoningCircleRecipe(CraftingRecipe):
     """A summoning circle"""
     name = "summoning circle"  # name to refer to this recipe as
+    crafting_time = 20
     tool_tags = []
     consumable_tags = ["blood"]
     output_prototypes = [
         "summoning_circle"
     ]
+    success_message = "You draw an arcane circle on the ground."
+
+
+class WoodenPuppetRecipe(CraftingRecipe):
+    """A puppet"""
+    name = "wooden puppet"  # name to refer to this recipe as
+    crafting_time = 15
+    tool_tags = ["blade"]
+    consumable_tags = ["wood"]
+    output_prototypes = [
+        {"key": "carved wooden doll",
+         "typeclass": "typeclasses.objects.Item",
+         "desc": "A small carved doll"}
+    ]

+ 3 - 4
world/spells.py

@@ -8,10 +8,9 @@ from utils.utils import has_effect
 
 def spell_light(caller, target, **kwargs):
     if not target:
-        caller.msg("You need something to place your light on.")
-        return
-
-    target_obj = caller.search(target, location=[caller, caller.location])
+        target_obj = caller
+    else:
+        target_obj = caller.search(target, location=[caller, caller.location])
 
     if not target_obj:
         return

+ 142 - 0
world/tutorial_prototypes.py

@@ -0,0 +1,142 @@
+"""
+Prototypes
+
+A prototype is a simple way to create individualized instances of a
+given typeclass. It is dictionary with specific key names.
+
+For example, you might have a Sword typeclass that implements everything a
+Sword would need to do. The only difference between different individual Swords
+would be their key, description and some Attributes. The Prototype system
+allows to create a range of such Swords with only minor variations. Prototypes
+can also inherit and combine together to form entire hierarchies (such as
+giving all Sabres and all Broadswords some common properties). Note that bigger
+variations, such as custom commands or functionality belong in a hierarchy of
+typeclasses instead.
+
+A prototype can either be a dictionary placed into a global variable in a
+python module (a 'module-prototype') or stored in the database as a dict on a
+special Script (a db-prototype). The former can be created just by adding dicts
+to modules Evennia looks at for prototypes, the latter is easiest created
+in-game via the `olc` command/menu.
+
+Prototypes are read and used to create new objects with the `spawn` command
+or directly via `evennia.spawn` or the full path `evennia.prototypes.spawner.spawn`.
+
+A prototype dictionary have the following keywords:
+
+Possible keywords are:
+- `prototype_key` - the name of the prototype. This is required for db-prototypes,
+  for module-prototypes, the global variable name of the dict is used instead
+- `prototype_parent` - string pointing to parent prototype if any. Prototype inherits
+  in a similar way as classes, with children overriding values in their partents.
+- `key` - string, the main object identifier.
+- `typeclass` - string, if not set, will use `settings.BASE_OBJECT_TYPECLASS`.
+- `location` - this should be a valid object or #dbref.
+- `home` - valid object or #dbref.
+- `destination` - only valid for exits (object or #dbref).
+- `permissions` - string or list of permission strings.
+- `locks` - a lock-string to use for the spawned object.
+- `aliases` - string or list of strings.
+- `attrs` - Attributes, expressed as a list of tuples on the form `(attrname, value)`,
+  `(attrname, value, category)`, or `(attrname, value, category, locks)`. If using one
+   of the shorter forms, defaults are used for the rest.
+- `tags` - Tags, as a list of tuples `(tag,)`, `(tag, category)` or `(tag, category, data)`.
+-  Any other keywords are interpreted as Attributes with no category or lock.
+   These will internally be added to `attrs` (eqivalent to `(attrname, value)`.
+
+See the `spawn` command and `evennia.prototypes.spawner.spawn` for more info.
+
+"""
+
+# ROOMS
+
+START_00 = {
+    "prototype_parent": "ROOM",
+    "prototype_tags": ["room"],
+    "key": "ruined room",
+    "aliases": "start_00",
+    "desc": "This room, once royally adorned, now lies in ruins. |/"
+            "A violent battle must have been fought in this place,"
+            "mixed with the broken wood of the furniture stand out broken weapons"
+            "and bodies devoured by the passage of time. "
+            "The long oak table that once occupied the center of the room"
+            " it's now overturned against the wall to create a makeshift barricade."
+}
+
+START_01 = {
+    "prototype_parent": "ROOM",
+    "prototype_tags": ["room"],
+    "key": "long hall",
+    "aliases": ["hall", "start_01"],
+    "desc": "A long hall paved with large hewn stones, thick oak beams"
+            "still hold up the ceiling frescoed with gilded symbols. Dust corpuscles swirl"
+            "in the light, disturbed by your passage."
+}
+
+START_02 = {
+    "prototype_parent": "ROOM",
+    "prototype_tags": ["room"],
+    "key": "old guardhouse",
+    "aliases": ["guardhouse", "start_02"],
+    "desc": "An old guardhouse devastated by the fighting that took place in these halls."
+    "The only part that has been spared is the ceiling, completely covered with"
+    "peeling frescoes depicting scenes of martial life."
+}
+
+START_03 = {
+    "prototype_parent": "ROOM",
+    "prototype_tags": ["room"],
+    "key": "empty corridor",
+    "aliases": ["corridor", "start_03"],
+    "desc": "The sides of the corridor are lined with stone archways, each adorned by a"
+            "stone statue. All the statues have been broken behind recognition."
+}
+
+START_04 = {
+    "prototype_parent": "ROOM",
+    "prototype_tags": ["room"],
+    "key": "empty corridor",
+    "aliases": ["corridor", "start_04"],
+    "desc": "This building seems to have survived the ravages of time better than"
+            "most of the others. Its arched roof and wide spaces suggests that"
+            "this is a temple or church of some kind."
+}
+
+# EXITS
+
+START_DOOR_00 = {
+    "prototype_parent": "EXIT",
+    "prototype_tags": ["exit"],
+    "key": "sculpted archway",
+    "aliases": ["archway", "start_door_00"],
+    "desc": "A beautifully sculpted arched entrance. Two figures are carved into the"
+            "stone on either side of the door, on the right Its, the muse of Deception, on"
+            "right Izzac, the muse of Authority."
+}
+
+START_DOOR_01 = {
+    "prototype_parent": "EXIT",
+    "prototype_tags": ["exit"],
+    "key": "open doorway",
+    "aliases": ["doorway", "start_door_01"],
+    "desc": "A large doorway, with no door. The rune '|y◧|n' is engraved on the granite jamb."
+}
+
+START_DOOR_02 = {
+    "prototype_parent": "EXIT",
+    "prototype_tags": ["exit"],
+    "key": "small doorway",
+    "aliases": ["doorway", "start_door_02"],
+    "desc": "A small doorway, with no door. The rune '|y◓|n' is engraved on the granite jamb."
+}
+
+START_DOOR_03 = {
+    "prototype_parent": "EXIT",
+    "prototype_tags": ["exit"],
+    "key": "reinforced door",
+    "aliases": ["door", "start_door_03"],
+    "desc": "A big oak door, reinforced with iron bars across its frame."
+            "It bears marks and burns all over its surface but hasn't been breached during the"
+            "siege."
+}
+