Browse Source

commit iniziale.

Francesco Cappelli 2 years ago
commit
043cdfc230
63 changed files with 5219 additions and 0 deletions
  1. 52 0
      .gitignore
  2. 40 0
      README.md
  3. 6 0
      __init__.py
  4. 14 0
      commands/README.md
  5. 0 0
      commands/__init__.py
  6. 285 0
      commands/builder.py
  7. 776 0
      commands/command.py
  8. 133 0
      commands/default_cmdsets.py
  9. 38 0
      server/README.md
  10. 1 0
      server/__init__.py
  11. 1 0
      server/conf/__init__.py
  12. 19 0
      server/conf/at_initial_setup.py
  13. 54 0
      server/conf/at_search.py
  14. 63 0
      server/conf/at_server_startstop.py
  15. 55 0
      server/conf/cmdparser.py
  16. 38 0
      server/conf/connection_screens.py
  17. 51 0
      server/conf/inlinefuncs.py
  18. 52 0
      server/conf/inputfuncs.py
  19. 30 0
      server/conf/lockfuncs.py
  20. 105 0
      server/conf/mssp.py
  21. 24 0
      server/conf/portal_services_plugins.py
  22. 24 0
      server/conf/server_services_plugins.py
  23. 37 0
      server/conf/serversession.py
  24. 50 0
      server/conf/settings.py
  25. 41 0
      server/conf/web_plugins.py
  26. 15 0
      server/logs/README.md
  27. 15 0
      typeclasses/README.md
  28. 0 0
      typeclasses/__init__.py
  29. 104 0
      typeclasses/accounts.py
  30. 62 0
      typeclasses/channels.py
  31. 80 0
      typeclasses/characters.py
  32. 81 0
      typeclasses/effects.py
  33. 171 0
      typeclasses/exits.py
  34. 41 0
      typeclasses/mob_actions.py
  35. 47 0
      typeclasses/mobs.py
  36. 264 0
      typeclasses/objects.py
  37. 287 0
      typeclasses/rooms.py
  38. 164 0
      typeclasses/scripts.py
  39. 0 0
      utils/__init__.py
  40. 63 0
      utils/building.py
  41. 1109 0
      utils/crafting.py
  42. 71 0
      utils/priodict.py
  43. 120 0
      utils/spath.py
  44. 34 0
      utils/utils.py
  45. 0 0
      web/__init__.py
  46. 13 0
      web/static_overrides/README.md
  47. 3 0
      web/static_overrides/webclient/css/README.md
  48. 3 0
      web/static_overrides/webclient/js/README.md
  49. 3 0
      web/static_overrides/website/css/README.md
  50. 3 0
      web/static_overrides/website/images/README.md
  51. 4 0
      web/template_overrides/README.md
  52. 3 0
      web/template_overrides/webclient/README.md
  53. 7 0
      web/template_overrides/website/README.md
  54. 3 0
      web/template_overrides/website/flatpages/README.md
  55. 3 0
      web/template_overrides/website/registration/README.md
  56. 18 0
      web/urls.py
  57. 10 0
      world/README.md
  58. 0 0
      world/__init__.py
  59. 26 0
      world/batch_cmds.ev
  60. 115 0
      world/batches/init.ev
  61. 219 0
      world/prototypes.py
  62. 23 0
      world/recipes_base.py
  63. 46 0
      world/spells.py

+ 52 - 0
.gitignore

@@ -0,0 +1,52 @@
+*.py[cod]
+
+# C extensions
+*.so
+
+# Packages
+*.egg
+*.egg-info
+dist
+build
+eggs
+parts
+var
+sdist
+develop-eggs
+.installed.cfg
+lib
+lib64
+__pycache__
+
+# Other
+*.swp
+*.log
+*.pid
+*.restart
+*.db3
+
+# Installation-specific.
+# For group efforts, comment out some or all of these.
+server/conf/secret_settings.py
+server/logs/*.log.*
+web/static/*
+web/media/*
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+nosetests.xml
+
+# Translations
+*.mo
+
+# Mr Developer
+.mr.developer.cfg
+.project
+.pydevproject
+
+# PyCharm config
+.idea

+ 40 - 0
README.md

@@ -0,0 +1,40 @@
+# Welcome to Evennia!
+
+This is your game directory, set up to let you start with
+your new game right away. An overview of this directory is found here:
+https://github.com/evennia/evennia/wiki/Directory-Overview#the-game-directory
+
+You can delete this readme file when you've read it and you can
+re-arrange things in this game-directory to suit your own sense of
+organisation (the only exception is the directory structure of the
+`server/` directory, which Evennia expects). If you change the structure
+you must however also edit/add to your settings file to tell Evennia
+where to look for things.
+
+Your game's main configuration file is found in
+`server/conf/settings.py` (but you don't need to change it to get
+started). If you just created this directory (which means you'll already
+have a `virtualenv` running if you followed the default instructions),
+`cd` to this directory then initialize a new database using
+
+    evennia migrate
+
+To start the server, stand in this directory and run
+
+    evennia start
+
+This will start the server, logging output to the console. Make
+sure to create a superuser when asked. By default you can now connect
+to your new game using a MUD client on `localhost`, port `4000`.  You can
+also log into the web client by pointing a browser to
+`http://localhost:4001`.
+
+# Getting started
+
+From here on you might want to look at one of the beginner tutorials:
+http://github.com/evennia/evennia/wiki/Tutorials.
+
+Evennia's documentation is here:
+https://github.com/evennia/evennia/wiki.
+
+Enjoy!

+ 6 - 0
__init__.py

@@ -0,0 +1,6 @@
+"""
+This sub-package holds the template for creating a new game folder.
+The new game folder (when running evennia --init) is a copy of this
+folder.
+
+"""

+ 14 - 0
commands/README.md

@@ -0,0 +1,14 @@
+# commands/
+
+This folder holds modules for implementing one's own commands and
+command sets. All the modules' classes are essentially empty and just
+imports the default implementations from Evennia; so adding anything
+to them will start overloading the defaults. 
+
+You can change the organisation of this directory as you see fit, just
+remember that if you change any of the default command set classes'
+locations, you need to add the appropriate paths to
+`server/conf/settings.py` so that Evennia knows where to find them.
+Also remember that if you create new sub directories you must put
+(optionally empty) `__init__.py` files in there so that Python can
+find your modules.

+ 0 - 0
commands/__init__.py


+ 285 - 0
commands/builder.py

@@ -0,0 +1,285 @@
+from evennia import default_cmds, create_object, search_tag
+from evennia.utils import inherits_from
+from evennia.utils.eveditor import EvEditor
+
+from commands.command import Command
+from typeclasses.exits import BaseDoor
+from typeclasses.rooms import Room, Zone
+
+def _descdoor_load(caller):
+    return caller.db.evmenu_target.db.desc or ""
+
+
+def _descdoor_save(caller, buf):
+    """
+    Save line buffer to the desc prop. This should
+    return True if successful and also report its status to the user.
+    """
+    caller.db.evmenu_target.setdesc(buf)
+    caller.msg("Saved.")
+    return True
+
+
+def _descdoor_quit(caller):
+    caller.attributes.remove("evmenu_target")
+    caller.msg("Exited editor.")
+
+class CmdDescDoor(Command):
+    """
+    describe a BaseDoor in the current room.
+
+    Usage:
+      descdoor <obj> = <description>
+
+    Switches:
+      edit - Open up a line editor for more advanced editing.
+
+    Sets the "desc" attribute on an object. If an object is not given,
+    describe the current room.
+    """
+
+    key = "descdoor"
+    aliases = ["descd"]
+    switch_options = ("edit",)
+    locks = "cmd:perm(descdoor) or perm(Builder)"
+    help_category = "Building"
+
+    def edit_handler(self):
+        if self.rhs:
+            self.msg("|rYou may specify a value, or use the edit switch, " "but not both.|n")
+            return
+        if self.args:
+            obj = self.caller.search(self.args)
+        else:
+            obj = self.caller.location or self.msg("|rYou can't describe oblivion.|n")
+        if not obj:
+            return
+
+        if not (obj.access(self.caller, "control") or obj.access(self.caller, "edit")):
+            self.caller.msg("You don't have permission to edit the description of %s." % obj.key)
+
+        self.caller.db.evmenu_target = obj
+        # launch the editor
+        EvEditor(
+            self.caller,
+            loadfunc=_descdoor_load,
+            savefunc=_descdoor_save,
+            quitfunc=_descdoor_quit,
+            key="desc",
+            persistent=True,
+        )
+        return
+
+    def func(self):
+        """Define command"""
+
+        caller = self.caller
+        if not self.args or ("=" in self.args and "edit" in self.switches):
+            caller.msg("Usage: descdoor <obj> = <description>")
+            return
+
+        if "edit" in self.switches:
+            self.edit_handler()
+            return
+
+        # We have an =
+        obj = caller.search(self.lhs)
+        if not obj:
+            return
+        desc = self.rhs or ""
+
+        if inherits_from(obj, BaseDoor):
+            if obj.access(self.caller, "control") or obj.access(self.caller, "edit"):
+                obj.setdesc(desc)
+                caller.msg("The description was set on %s." % obj.get_display_name(caller))
+            else:
+                caller.msg("You don't have permission to edit the description of %s." % obj.key)
+        else:
+            self.msg("|rYou can't describe oblivion.|n")
+
+
+class CmdOpen(default_cmds.CmdOpen):
+    __doc__ = default_cmds.CmdOpen.__doc__
+    # overloading parts of the default CmdOpen command to support doors.
+
+    def create_exit(self, exit_name, location, destination, exit_aliases=None, typeclass=None):
+        """
+        Simple wrapper for the default CmdOpen.create_exit
+        """
+        # create a new exit as normal
+        new_exit = super().create_exit(
+            exit_name, location, destination, exit_aliases=exit_aliases, typeclass=typeclass
+        )
+        if hasattr(self, "return_exit_already_created"):
+            # we don't create a return exit if it was already created (because
+            # we created a door)
+            del self.return_exit_already_created
+            return new_exit
+        if inherits_from(new_exit, BaseDoor):
+            # a door - create its counterpart and make sure to turn off the default
+            # return-exit creation of CmdOpen
+            self.caller.msg(
+                "Note: A door-type exit was created - ignored eventual custom return-exit type."
+            )
+            self.return_exit_already_created = True
+            back_exit = self.create_exit(
+                exit_name, destination, location, exit_aliases=exit_aliases, typeclass=typeclass
+            )
+            new_exit.db.return_exit = back_exit
+            back_exit.db.return_exit = new_exit
+        return new_exit
+
+class CmdUpdateLightState(Command):
+    """
+    update room light state.
+
+    Usage:
+      update_light [targetroom]
+    """
+
+    key = "update_light"
+    aliases = ["up_l"]
+    locks = "cmd:perm(update_light) or perm(Builder)"
+    help_category = "Building"
+    arg_regex = r"(?:^(?:\s+|\/).*$)|^$"
+
+    def func(self):
+        caller = self.caller
+        if not self.args:
+            target = caller.location
+            if not target:
+                caller.msg("You have no location to update!")
+                return
+        else:
+            target = caller.search(self.args)
+            if not (target.attributes.has("is_lit") and hasattr(target, "check_light_state")):
+                caller.msg(
+                    "You cannot update lights on {}!".format(target.key))
+                return
+
+        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]
+
+    Creates a new zone.
+
+    """
+
+    key = "zone"
+    locks = "cmd:perm(zone) or perm(Builders)"
+    help_category = "Building"
+
+    def func(self):
+        """
+        Creates the zone.
+        """
+
+        caller = self.caller
+
+        if "list" in self.switches:
+            string = "";
+            zones = search_tag(key="zone", category="general")
+            for zone in zones:
+                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)
+
+            caller.msg("Zones found: \n" + string)
+            return
+
+        if not self.args:
+            string = "Usage: zone[/list||/del||/addroom] [zonename] [= room]"
+            caller.msg(string)
+            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))
+
+    def delete_zone(self, zone_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
+
+        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
+
+    Adds a room to an existing zone.
+    """
+    key = "@addtozone"
+    locks = "cmd:perm(zone) or perm(Builders)"
+    help_category = "Building"
+
+    def func(self):
+        """
+        Adds a room to an existing zone.
+        """
+
+        caller = self.caller
+
+        if self.rhs:
+            # We have an =
+            zone = caller.search(self.rhs, global_search=True, exact=True)
+            if not zone:
+                self.msg("Zone %s doesn't exist." % self.rhs)
+                return
+            if not utils.inherits_from(zone, Zone):
+                self.msg("{r%s is not a valid zone.{n" % zone.name)
+                return
+
+            room = caller.search(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)
+                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))
+
+        else:
+            self.msg("{rUsage: @addtozone obj = zone{n")
+            return

+ 776 - 0
commands/command.py

@@ -0,0 +1,776 @@
+"""
+Commands
+
+Commands describe the input the account can do to the game.
+
+"""
+import re
+import random
+
+from evennia import utils, default_cmds, create_script, logger
+from evennia.utils import evtable
+from evennia.contrib.health_bar import display_meter
+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.utils import has_tag, fmt_light, fmt_dark, toggle_effect, has_effect, indefinite_article
+from typeclasses.exits import BaseDoor
+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
+
+
+class Command(default_cmds.MuxCommand):
+    """
+    Inherit from this if you want to create your own command styles
+    from scratch.  Note that Evennia's default commands inherits from
+    MuxCommand instead.
+
+    Note that the class's `__doc__` string (this text) is
+    used by Evennia to create the automatic help entry for
+    the command, so make sure to document consistently here.
+
+    Each Command implements the following methods, called
+    in this order (only func() is actually required):
+        - at_pre_cmd(): If this returns anything truthy, execution is aborted.
+        - parse(): Should perform any extra parsing needed on self.args
+            and store the result on self.
+        - func(): Performs the actual work.
+        - at_post_cmd(): Extra actions, often things done after
+            every command, like prompts.
+
+    """
+
+    def at_post_cmd(self):
+        caller = self.caller
+        prompt = "|_|/°|w%s|n°: " % (caller.location)
+        caller.msg(prompt=prompt)
+
+
+# overloading the look command with our custom MuxCommand with at_post_cmd hook
+class CmdLook(default_cmds.CmdLook, Command):
+    pass
+
+
+class CmdDrop(default_cmds.CmdDrop, Command):
+    pass
+
+
+class CmdGet(Command):
+    """
+        pick up something
+
+        Usage:
+          get <obj> [from <container>]
+
+        Picks up an object from your location and puts it in
+        your inventory.
+        """
+
+    key = "get"
+    aliases = "grab"
+    locks = "cmd:all()"
+    arg_regex = r"\s|$"
+
+    def parse(self):
+        """
+        Handle parsing of:
+            <object> [FROM <container>]
+        """
+        self.args = args = self.args.strip().lower()
+
+        if "from" in args:
+            obj, *rest = args.split(" from ", 1)
+            container = rest[0] if rest else ""
+        else:
+            obj = self.args
+            container = ""
+
+        self.obj_name = obj.strip()
+        self.container_name = container.strip()
+
+    def func(self):
+        """implements the command."""
+        caller = self.caller
+
+        if not self.args or not self.obj_name:
+            caller.msg("Get what?")
+            return
+
+        if inherits_from(caller.location, IndoorRoom) and not caller.location.db.is_lit:
+            caller.msg("Its too dark to get anything.")
+            return
+
+        if self.container_name:
+            container = caller.search(self.container_name, location=caller.location)
+            obj = caller.search(self.obj_name, location=container)
+        else:
+            obj = caller.search(self.obj_name, location=caller.location)
+
+        if not obj:
+            return
+        if caller == obj:
+            caller.msg("You can't get yourself.")
+            return
+        if not obj.access(caller, "get"):
+            if obj.db.get_err_msg:
+                caller.msg(obj.db.get_err_msg)
+            else:
+                caller.msg("You can't get that.")
+            return
+
+        # calling at_before_get hook method
+        if not obj.at_before_get(caller):
+            return
+
+        success = obj.move_to(caller, quiet=True)
+        if not success:
+            caller.msg("This can't be picked up.")
+        else:
+            caller.msg("You pick up %s." % obj.name)
+            caller.location.msg_contents(
+                "%s picks up %s." % (caller.name, obj.name), exclude=caller
+            )
+            # calling at_get hook method
+            obj.at_get(caller)
+
+
+class CmdCharacter(Command):
+    """
+    Show character sheet.
+
+    Usage:
+      character
+
+    Displays a list of your current ability values.
+    """
+    key = "character"
+    aliases = ["char"]
+    lock = "cmd:all()"
+    help_category = "General"
+
+    def func(self):
+        self.caller.msg(
+            '\u250B|w' + " {0:27}".format(self.caller.name,) + '|n\u250B|/')
+
+        self.caller.msg('-HEALTH' + '-' * 23)
+        meter = display_meter(self.caller.get_health(), 100,
+                              empty_color="222", align="center", fill_color=["R"])
+        self.caller.msg(meter)
+        self.caller.msg('-MANA' + '-' * 25)
+        meter = display_meter(self.caller.get_mana(), 100,
+                              empty_color="222", align="center", fill_color=["B"])
+        self.caller.msg(meter + '|/')
+        str, agi, intel = self.caller.get_abilities()
+        table = evtable.EvTable(border="tablecols")
+        table.add_column("STRENGTH", "AGILITY",
+                         "INTELLECT", header="Attributes")
+        table.add_column("|g%03d|n/100" % str, "|g%03d|n/100" %
+                         agi, "|g%03d|n/100" % intel, header="")
+        self.caller.msg(table)
+
+
+class CmdLight(Command):
+    """
+    A command to light objects that support it.
+
+    Usage:
+      light <someone>
+
+    Light an object in your inventory.
+    """
+
+    key = "light"
+    aliases = []
+    lock = "cmd:all()"
+    help_category = "General"
+    arg_regex = r"(?:^(?:\s+|\/).*$)|^$"
+
+    def func(self):
+        caller = self.caller
+        if not self.args:
+            self.msg("Light what?")
+            return
+        else:
+            target = caller.search(self.args)
+            if not target:
+                self.msg("Light what?")
+                return
+
+            if not target.access(caller, 'light'):
+                self.msg("You cannot do that.")
+                return
+
+            if not has_effect(target, "emit_light"):
+                toggle_effect(target, "emit_light")
+                self.msg("You light {}.".format(target.name))
+            else:
+                toggle_effect(target, "emit_light")
+                self.msg("You put off {}.".format(target.name))
+
+            if caller.location and inherits_from(caller.location, IndoorRoom):
+                caller.location.check_light_state()
+
+
+class CmdSearch(Command):
+    """
+    """
+
+    key = "search"
+    aliases = ["ss"]
+    locks = "cmd:all()"
+    help_category = "General"
+    arg_regex = r"(?:^(?:\s+|\/).*$)|^$"
+
+    def func(self):
+        caller = self.caller
+
+        if has_effect(caller, "is_busy"):
+            caller.msg("You are already busy {}.".format(caller.current_action.busy_msg()))
+            return
+
+        if not self.args:
+            target = caller.location
+            if not target:
+                caller.msg("You have no location to search!")
+                return
+        else:
+            target = caller.search(self.args)
+            if not target:
+                caller.msg("You cannot search {}!".format(self.args))
+                return
+        if target.access(caller, "search"):
+            toggle_effect(caller, "is_busy")
+            caller.msg("You search {}.".format(target.get_display_name(caller)))
+            action_script = create_script("commands.command.CmdSearchComplete", obj=caller, interval=CMD_SEARCH_TIME, attributes=[("target", self.args)])
+            caller.db.current_action = action_script
+        else:
+            caller.msg("You cannot search {}!".format(target.get_display_name(caller)))
+
+
+class CmdSearchComplete(CmdActionScript):
+    def at_script_creation(self):
+        super().at_script_creation()
+
+        self.key = "cmd_search_complete"
+        self.desc = ""
+        self.db.target = ""
+
+    def at_repeat(self):
+        caller = self.obj
+        target_string = self.db.target
+
+        if has_effect(caller, "is_busy"):
+            toggle_effect(caller, "is_busy")
+
+        if not target_string:
+            target = caller.location
+            if not target:
+                caller.msg("You have no location to search!")
+                return
+        else:
+            target = caller.search(target_string)
+            if not target:
+                caller.msg("You cannot search {} anymore!".format(target_string))
+                return
+        if target.access(caller, "search"):
+            items = []
+            for con in target.contents:
+                if inherits_from(con, Item) and con.access(caller, "get"):
+                    items.append(con)
+
+            if items:
+                found_item_idx = random.randrange(0, len(items))
+                found_item = items[found_item_idx]
+                found_item_name = found_item.get_numbered_name(1, caller)[0]
+                caller.msg("You find {}.".format(found_item_name))
+
+                if not found_item.access(caller, "get"):
+                    if found_item.db.get_err_msg:
+                        caller.msg(found_item.db.get_err_msg)
+                    else:
+                        caller.msg("You can't get that.")
+                    return
+
+                # calling at_before_get hook method
+                if not found_item.at_before_get(caller):
+                    return
+
+                success = found_item.move_to(caller, quiet=True)
+                if not success:
+                    caller.msg("This can't be picked up.")
+                else:
+                    caller.msg("You pick up %s." % found_item_name)
+                    caller.location.msg_contents(
+                        "%s picks up %s." % (caller.name, found_item_name), exclude=caller
+                    )
+                    # calling at_get hook method
+                    found_item.at_get(caller)
+            else:
+                caller.msg("There is nothing to be found here.")
+        else:
+            caller.msg("You cannot search {} anymore!".format(target.get_display_name(caller)))
+
+    def busy_msg(self):
+        return "searching {}".format(self.db.target)
+
+
+class CmdPut(Command):
+    """
+    Put an item inside a container
+
+    Usage:
+        put <item> in <container>
+    """
+
+    key = "put"
+    locks = "cmd:all()"
+    help_category = "General"
+
+    def parse(self):
+        """
+        Handle parsing of:
+            <item> in <container>
+        """
+        self.args = args = self.args.strip().lower()
+        item_name, container_name = "", ""
+
+        if " in " in args:
+            item_name, *rest = args.split(" in ", 1)
+            container_name = rest[0] if rest else ""
+
+        self.item_name = item_name.strip()
+        self.container_name = container_name.strip()
+
+    def func(self):
+        caller = self.caller
+
+        if not self.args or not self.item_name or not self.container_name:
+            caller.msg("Usage: put <item> in <container>")
+            return
+
+        item = caller.search(self.item_name, typeclass="typeclasses.objects.Item")
+        if not item:
+            return
+
+        container = caller.search(self.container_name, typeclass="typeclasses.objects.ContainerFeature")
+        if not container:
+            return
+
+        if not item.access(caller, "get"):
+            caller.msg("You cannot do that with {}.".format(item.name))
+            return
+
+        if not container.access(caller, "put"):
+            caller.msg("You cannot access {}.".format(container.name))
+            return
+
+        if has_tag(item, "equipped", "general"):
+            caller.msg("{} is equipped. Remove it first.".format(item.name))
+            return
+
+        item.move_to(container, use_destination=False)
+
+        caller.msg("You put {} inside {}.".format(item.name, container.name))
+
+
+
+class CmdOpenCloseDoor(Command):
+    """
+    Open and close a door
+
+    Usage:
+        open <door>
+        close <door>
+
+    """
+
+    key = "op"
+    aliases = ["close"]
+    locks = "cmd:all()"
+    help_category = "General"
+
+    def func(self):
+        if not self.args:
+            self.caller.msg("Usage: open||close <door>")
+            return
+
+        door = self.caller.search(self.args)
+        if not door:
+            return
+        if not inherits_from(door, BaseDoor):
+            self.caller.msg("This is not a door.")
+            return
+
+        if self.cmdstring == "op":
+            if door.locks.check(self.caller, "traverse"):
+                self.caller.msg("%s is already open." % door.key)
+            else:
+                door.setlock("traverse:true()")
+                self.caller.msg("You open %s." % door.key)
+        else:  # close
+            if not door.locks.check(self.caller, "traverse"):
+                self.caller.msg("%s is already closed." % door.key)
+            else:
+                door.setlock("traverse:false()")
+                self.caller.msg("You close %s." % door.key)
+
+
+class CmdEquip(Command):
+    key = "equip"
+    aliases = ["eq", "remove"]
+    lock = "cmd:all()"
+    help_category = "General"
+    arg_regex = r"\s.+|$"
+
+    def func(self):
+        caller = self.caller
+
+        if not self.args:
+            caller.msg("You cannot do that.")
+            return
+
+        item = caller.search(self.args, location=caller, nofound_string="")
+        if not item:
+            caller.msg("You don't have any {}".format(self.args))
+            return
+
+        if inherits_from(item, EquippableItem) and item.access(caller, "equip"):
+            if self.cmdstring == "remove":
+                if has_tag(item, "equipped", "general"):
+                    if item.at_unequip(caller, ""):
+                        self.remove(caller, item)
+                    else:
+                        return
+                else:
+                    caller.msg("{} is not equipped.".format(item.name))
+                return
+            else:
+                if has_tag(item, "equipped", "general"):
+                    caller.msg("{} is already equipped.".format(item.name))
+                    return
+
+                slots = [slot for slot in caller.db.equipment.keys() if item.db.slot in slot]
+                if not slots:
+                    caller.msg("You don't have {} {}".format(indefinite_article(item.db.slot), item.db.slot))
+                    return
+
+                selected_slot = slots[0]
+
+                # find an empty slot
+                empty_slots = [slot for slot in slots if not caller.db.equipment[slot]]
+                if empty_slots:
+                    selected_slot = empty_slots[0]
+
+                # remove equipment if slot is already in use
+                if caller.db.equipment[selected_slot]:
+                    old_equipment = caller.search(caller.db.equipment[selected_slot], location=caller, quiet=True)
+                    if old_equipment:
+                        if old_equipment[0].at_unequip(caller, selected_slot):
+                            self.remove(caller, old_equipment[0])
+                        else:
+                            return
+
+                if item.at_equip(caller, selected_slot):
+                    caller.db.equipment[selected_slot] = item.dbref
+                    item.tags.add("equipped", category="general")
+
+                    caller.msg("You equip |w{}|n on |w{}|n".format(item.name, selected_slot))
+                    caller.location.msg_contents("{} equips |w{}|n on {}".format(caller.name, item.name, selected_slot), exclude=caller)
+
+        else:
+            caller.msg("You cannot equip {}".format(item.name))
+
+    def remove(self, caller, item):
+        selected_slot = [slot for slot in caller.db.equipment.keys() if caller.db.equipment[slot] == item.dbref][0]
+        caller.msg("You remove {} from {}".format(item, selected_slot))
+        caller.db.equipment[selected_slot] = None
+        item.tags.remove("equipped", category="general")
+
+
+class CmdInventory(Command):
+    """
+    view inventory
+
+    Usage:
+      inventory
+      inv
+
+    Shows your inventory.
+    """
+
+    key = "inventory"
+    aliases = ["inv", "i"]
+    locks = "cmd:all()"
+    arg_regex = r"$"
+
+    def func(self):
+        """check inventory"""
+        items = self.caller.contents
+        if not items:
+            string = "You are not carrying anything."
+        else:
+            from evennia.utils.ansi import raw as raw_ansi
+
+            table = self.styled_table()
+            for item in items:
+                effect_string = ""
+
+                if has_tag(item, 'equipped', 'general'):
+                    effect_string += "|w⚒|n"
+                else:
+                    effect_string += "|=a⚒|n"
+
+                if has_effect(item, 'emit_light'):
+                    effect_string += "|y☀|n"
+                else:
+                    effect_string += "|=a☀|n"
+
+                table.add_row(
+                    f"|w{item.name}|n",
+                    effect_string,
+                    "{}|n".format(utils.crop(raw_ansi(item.db.desc), width=50) or ""),
+                )
+            string = f"|wYou are carrying:\n{table}"
+        self.caller.msg(string)
+
+
+class CmdCast(Command):
+    """
+
+    """
+    key = "cast"
+    aliases = ["cs"]
+    lock = "cmd:false()"
+    help_category = "General"
+    arg_regex = r"\s.+|$"
+
+    def parse(self):
+        """
+        Handle parsing of:
+        ::
+            <spell> [at <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 ""
+        else:
+            spell_name = args
+
+        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("Usage: cast <spell> [at <target>]")
+            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"]
+    lock = "cmd:false()"
+    help_category = "General"
+    arg_regex = r"\s.+|$"
+
+    def func(self):
+        caller = self.caller
+
+        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)
+
+        # caller.msg(dkmud_oob=({"testarg": "valuetestarg"}))
+
+        # if not self.args:
+        #     target = caller.location
+        #     if not target:
+        #         caller.msg("You have no location to test!")
+        #         return
+        # else:
+        #     target = caller.search(self.args)
+        #     if not target:
+        #         return
+
+
+# -------------------------------------------------------------
+#
+# The default commands inherit from
+#
+#   evennia.commands.default.muxcommand.MuxCommand.
+#
+# If you want to make sweeping changes to default commands you can
+# uncomment this copy of the MuxCommand parent and add
+#
+#   COMMAND_DEFAULT_CLASS = "commands.command.MuxCommand"
+#
+# to your settings file. Be warned that the default commands expect
+# the functionality implemented in the parse() method, so be
+# careful with what you change.
+#
+# -------------------------------------------------------------
+
+# from evennia.utils import utils
+#
+#
+# class MuxCommand(Command):
+#     """
+#     This sets up the basis for a MUX command. The idea
+#     is that most other Mux-related commands should just
+#     inherit from this and don't have to implement much
+#     parsing of their own unless they do something particularly
+#     advanced.
+#
+#     Note that the class's __doc__ string (this text) is
+#     used by Evennia to create the automatic help entry for
+#     the command, so make sure to document consistently here.
+#     """
+#     def has_perm(self, srcobj):
+#         """
+#         This is called by the cmdhandler to determine
+#         if srcobj is allowed to execute this command.
+#         We just show it here for completeness - we
+#         are satisfied using the default check in Command.
+#         """
+#         return super().has_perm(srcobj)
+#
+#     def at_pre_cmd(self):
+#         """
+#         This hook is called before self.parse() on all commands
+#         """
+#         pass
+#
+#     def at_post_cmd(self):
+#         """
+#         This hook is called after the command has finished executing
+#         (after self.func()).
+#         """
+#         pass
+#
+#     def parse(self):
+#         """
+#         This method is called by the cmdhandler once the command name
+#         has been identified. It creates a new set of member variables
+#         that can be later accessed from self.func() (see below)
+#
+#         The following variables are available for our use when entering this
+#         method (from the command definition, and assigned on the fly by the
+#         cmdhandler):
+#            self.key - the name of this command ('look')
+#            self.aliases - the aliases of this cmd ('l')
+#            self.permissions - permission string for this command
+#            self.help_category - overall category of command
+#
+#            self.caller - the object calling this command
+#            self.cmdstring - the actual command name used to call this
+#                             (this allows you to know which alias was used,
+#                              for example)
+#            self.args - the raw input; everything following self.cmdstring.
+#            self.cmdset - the cmdset from which this command was picked. Not
+#                          often used (useful for commands like 'help' or to
+#                          list all available commands etc)
+#            self.obj - the object on which this command was defined. It is often
+#                          the same as self.caller.
+#
+#         A MUX command has the following possible syntax:
+#
+#           name[ with several words][/switch[/switch..]] arg1[,arg2,...] [[=|,] arg[,..]]
+#
+#         The 'name[ with several words]' part is already dealt with by the
+#         cmdhandler at this point, and stored in self.cmdname (we don't use
+#         it here). The rest of the command is stored in self.args, which can
+#         start with the switch indicator /.
+#
+#         This parser breaks self.args into its constituents and stores them in the
+#         following variables:
+#           self.switches = [list of /switches (without the /)]
+#           self.raw = This is the raw argument input, including switches
+#           self.args = This is re-defined to be everything *except* the switches
+#           self.lhs = Everything to the left of = (lhs:'left-hand side'). If
+#                      no = is found, this is identical to self.args.
+#           self.rhs: Everything to the right of = (rhs:'right-hand side').
+#                     If no '=' is found, this is None.
+#           self.lhslist - [self.lhs split into a list by comma]
+#           self.rhslist - [list of self.rhs split into a list by comma]
+#           self.arglist = [list of space-separated args (stripped, including '=' if it exists)]
+#
+#           All args and list members are stripped of excess whitespace around the
+#           strings, but case is preserved.
+#         """
+#         raw = self.args
+#         args = raw.strip()
+#
+#         # split out switches
+#         switches = []
+#         if args and len(args) > 1 and args[0] == "/":
+#             # we have a switch, or a set of switches. These end with a space.
+#             switches = args[1:].split(None, 1)
+#             if len(switches) > 1:
+#                 switches, args = switches
+#                 switches = switches.split('/')
+#             else:
+#                 args = ""
+#                 switches = switches[0].split('/')
+#         arglist = [arg.strip() for arg in args.split()]
+#
+#         # check for arg1, arg2, ... = argA, argB, ... constructs
+#         lhs, rhs = args, None
+#         lhslist, rhslist = [arg.strip() for arg in args.split(',')], []
+#         if args and '=' in args:
+#             lhs, rhs = [arg.strip() for arg in args.split('=', 1)]
+#             lhslist = [arg.strip() for arg in lhs.split(',')]
+#             rhslist = [arg.strip() for arg in rhs.split(',')]
+#
+#         # save to object properties:
+#         self.raw = raw
+#         self.switches = switches
+#         self.args = args.strip()
+#         self.arglist = arglist
+#         self.lhs = lhs
+#         self.lhslist = lhslist
+#         self.rhs = rhs
+#         self.rhslist = rhslist
+#
+#         # if the class has the account_caller property set on itself, we make
+#         # sure that self.caller is always the account if possible. We also create
+#         # a special property "character" for the puppeted object, if any. This
+#         # is convenient for commands defined on the Account only.
+#         if hasattr(self, "account_caller") and self.account_caller:
+#             if utils.inherits_from(self.caller, "evennia.objects.objects.DefaultObject"):
+#                 # caller is an Object/Character
+#                 self.character = self.caller
+#                 self.caller = self.caller.account
+#             elif utils.inherits_from(self.caller, "evennia.accounts.accounts.DefaultAccount"):
+#                 # caller was already an Account
+#                 self.character = self.caller.get_puppet(self.session)
+#             else:
+#                 self.character = None

+ 133 - 0
commands/default_cmdsets.py

@@ -0,0 +1,133 @@
+"""
+Command sets
+
+All commands in the game must be grouped in a cmdset.  A given command
+can be part of any number of cmdsets and cmdsets can be added/removed
+and merged onto entities at runtime.
+
+To create new commands to populate the cmdset, see
+`commands/command.py`.
+
+This module wraps the default command sets of Evennia; overloads them
+to add/remove commands from the default lineup. You can create your
+own cmdsets by inheriting from them or directly from `evennia.CmdSet`.
+
+"""
+
+from evennia import default_cmds
+from commands.command import CmdCharacter
+from commands.command import CmdLook
+from commands.command import CmdGet
+from commands.command import CmdDrop
+from commands.command import CmdLight
+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
+
+from commands.builder import CmdUpdateLightState
+from commands.builder import CmdOpen
+from commands.builder import CmdDescDoor
+from commands.builder import CmdZone
+
+from utils.crafting import CmdCraft
+
+
+class CharacterCmdSet(default_cmds.CharacterCmdSet):
+    """
+    The `CharacterCmdSet` contains general in-game commands like `look`,
+    `get`, etc available on in-game Character objects. It is merged with
+    the `AccountCmdSet` when an Account puppets a Character.
+    """
+
+    key = "DefaultCharacter"
+
+    def at_cmdset_creation(self):
+        """
+        Populates the cmdset
+        """
+        super().at_cmdset_creation()
+        #
+        # any commands you add below will overload the default ones.
+        #
+        self.add(CmdCharacter())
+        self.add(CmdLook())
+        self.add(CmdGet())
+        self.add(CmdDrop())
+        self.add(CmdTestPy())
+        self.add(CmdLight())
+        self.add(CmdSearch())
+        self.add(CmdEquip())
+        self.add(CmdInventory())
+        self.add(CmdCast())
+        self.add(CmdPut())
+        self.add(CmdOpenCloseDoor())
+
+        self.add(CmdUpdateLightState())
+        self.add(CmdOpen())
+        self.add(CmdDescDoor())
+        self.add(CmdZone())
+
+        self.add(CmdCraft())
+
+class AccountCmdSet(default_cmds.AccountCmdSet):
+    """
+    This is the cmdset available to the Account at all times. It is
+    combined with the `CharacterCmdSet` when the Account puppets a
+    Character. It holds game-account-specific commands, channel
+    commands, etc.
+    """
+
+    key = "DefaultAccount"
+
+    def at_cmdset_creation(self):
+        """
+        Populates the cmdset
+        """
+        super().at_cmdset_creation()
+        #
+        # any commands you add below will overload the default ones.
+        #
+
+
+class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet):
+    """
+    Command set available to the Session before being logged in.  This
+    holds commands like creating a new account, logging in, etc.
+    """
+
+    key = "DefaultUnloggedin"
+
+    def at_cmdset_creation(self):
+        """
+        Populates the cmdset
+        """
+        super().at_cmdset_creation()
+        #
+        # any commands you add below will overload the default ones.
+        #
+
+
+class SessionCmdSet(default_cmds.SessionCmdSet):
+    """
+    This cmdset is made available on Session level once logged in. It
+    is empty by default.
+    """
+
+    key = "DefaultSession"
+
+    def at_cmdset_creation(self):
+        """
+        This is the only method defined in a cmdset, called during
+        its creation. It should populate the set with command instances.
+
+        As and example we just add the empty base `Command` object.
+        It prints some info.
+        """
+        super().at_cmdset_creation()
+        #
+        # any commands you add below will overload the default ones.
+        #

+ 38 - 0
server/README.md

@@ -0,0 +1,38 @@
+# server/ 
+
+This directory holds files used by and configuring the Evennia server 
+itself.
+
+Out of all the subdirectories in the game directory, Evennia does
+expect this directory to exist, so you should normally not delete,
+rename or change its folder structure.
+
+When running you will find four new files appear in this directory: 
+
+ - `server.pid` and `portal.pid`: These hold the process IDs of the
+   Portal and Server, so that they can be managed by the launcher. If
+   Evennia is shut down uncleanly (e.g. by a crash or via a kill
+   signal), these files might erroneously remain behind. If so Evennia
+   will tell you they are "stale" and they can be deleted manually.
+ - `server.restart` and `portal.restart`: These hold flags to tell the
+   server processes if it should die or start again. You never need to
+   modify those files.
+ - `evennia.db3`: This will only appear if you are using the default
+   SQLite3 database; it a binary file that holds the entire game
+   database; deleting this file will effectively reset the game for
+   you and you can start fresh with `evennia migrate` (useful during
+   development).  
+
+## server/conf/
+
+This subdirectory holds the configuration modules for the server. With
+them you can change how Evennia operates and also plug in your own
+functionality to replace the default. You usually need to restart the
+server to apply changes done here. The most important file is the file
+`settings.py` which is the main configuration file of Evennia. 
+
+## server/logs/
+
+This subdirectory holds various log files created by the running
+Evennia server. It is also the default location for storing any custom
+log files you might want to output using Evennia's logging mechanisms.

+ 1 - 0
server/__init__.py

@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-

+ 1 - 0
server/conf/__init__.py

@@ -0,0 +1 @@
+# -*- coding: utf-8 -*-

+ 19 - 0
server/conf/at_initial_setup.py

@@ -0,0 +1,19 @@
+"""
+At_initial_setup module template
+
+Custom at_initial_setup method. This allows you to hook special
+modifications to the initial server startup process. Note that this
+will only be run once - when the server starts up for the very first
+time! It is called last in the startup process and can thus be used to
+overload things that happened before it.
+
+The module must contain a global function at_initial_setup().  This
+will be called without arguments. Note that tracebacks in this module
+will be QUIETLY ignored, so make sure to check it well to make sure it
+does what you expect it to.
+
+"""
+
+
+def at_initial_setup():
+    pass

+ 54 - 0
server/conf/at_search.py

@@ -0,0 +1,54 @@
+"""
+Search and multimatch handling
+
+This module allows for overloading two functions used by Evennia's
+search functionality:
+
+    at_search_result:
+        This is called whenever a result is returned from an object
+        search (a common operation in commands).  It should (together
+        with at_multimatch_input below) define some way to present and
+        differentiate between multiple matches (by default these are
+        presented as 1-ball, 2-ball etc)
+    at_multimatch_input:
+        This is called with a search term and should be able to
+        identify if the user wants to separate a multimatch-result
+        (such as that from a previous search). By default, this
+        function understands input on the form 1-ball, 2-ball etc as
+        indicating that the 1st or 2nd match for "ball" should be
+        used.
+
+This module is not called by default, to use it, add the following
+line to your settings file:
+
+    SEARCH_AT_RESULT = "server.conf.at_search.at_search_result"
+
+"""
+
+
+def at_search_result(matches, caller, query="", quiet=False, **kwargs):
+    """
+    This is a generic hook for handling all processing of a search
+    result, including error reporting.
+
+    Args:
+        matches (list): This is a list of 0, 1 or more typeclass instances,
+            the matched result of the search. If 0, a nomatch error should
+            be echoed, and if >1, multimatch errors should be given. Only
+            if a single match should the result pass through.
+        caller (Object): The object performing the search and/or which should
+        receive error messages.
+    query (str, optional): The search query used to produce `matches`.
+        quiet (bool, optional): If `True`, no messages will be echoed to caller
+            on errors.
+
+    Keyword Args:
+        nofound_string (str): Replacement string to echo on a notfound error.
+        multimatch_string (str): Replacement string to echo on a multimatch error.
+
+    Returns:
+        processed_result (Object or None): This is always a single result
+            or `None`. If `None`, any error reporting/handling should
+            already have happened.
+
+    """

+ 63 - 0
server/conf/at_server_startstop.py

@@ -0,0 +1,63 @@
+"""
+Server startstop hooks
+
+This module contains functions called by Evennia at various
+points during its startup, reload and shutdown sequence. It
+allows for customizing the server operation as desired.
+
+This module must contain at least these global functions:
+
+at_server_start()
+at_server_stop()
+at_server_reload_start()
+at_server_reload_stop()
+at_server_cold_start()
+at_server_cold_stop()
+
+"""
+
+
+def at_server_start():
+    """
+    This is called every time the server starts up, regardless of
+    how it was shut down.
+    """
+    pass
+
+
+def at_server_stop():
+    """
+    This is called just before the server is shut down, regardless
+    of it is for a reload, reset or shutdown.
+    """
+    pass
+
+
+def at_server_reload_start():
+    """
+    This is called only when server starts back up after a reload.
+    """
+    pass
+
+
+def at_server_reload_stop():
+    """
+    This is called only time the server stops before a reload.
+    """
+    pass
+
+
+def at_server_cold_start():
+    """
+    This is called only when the server starts "cold", i.e. after a
+    shutdown or a reset.
+    """
+    pass
+
+
+def at_server_cold_stop():
+    """
+    This is called only when the server goes down due to a shutdown or
+    reset.
+    """
+    pass

+ 55 - 0
server/conf/cmdparser.py

@@ -0,0 +1,55 @@
+"""
+Changing the default command parser
+
+The cmdparser is responsible for parsing the raw text inserted by the
+user, identifying which command/commands match and return one or more
+matching command objects. It is called by Evennia's cmdhandler and
+must accept input and return results on the same form. The default
+handler is very generic so you usually don't need to overload this
+unless you have very exotic parsing needs; advanced parsing is best
+done at the Command.parse level.
+
+The default cmdparser understands the following command combinations
+(where [] marks optional parts.)
+
+[cmdname[ cmdname2 cmdname3 ...] [the rest]
+
+A command may consist of any number of space-separated words of any
+length, and contain any character. It may also be empty.
+
+The parser makes use of the cmdset to find command candidates. The
+parser return a list of matches. Each match is a tuple with its first
+three elements being the parsed cmdname (lower case), the remaining
+arguments, and the matched cmdobject from the cmdset.
+
+
+This module is not accessed by default. To tell Evennia to use it
+instead of the default command parser, add the following line to
+your settings file:
+
+    COMMAND_PARSER = "server.conf.cmdparser.cmdparser"
+
+"""
+
+
+def cmdparser(raw_string, cmdset, caller, match_index=None):
+    """
+    This function is called by the cmdhandler once it has
+    gathered and merged all valid cmdsets valid for this particular parsing.
+
+    raw_string - the unparsed text entered by the caller.
+    cmdset - the merged, currently valid cmdset
+    caller - the caller triggering this parsing
+    match_index - an optional integer index to pick a given match in a
+                  list of same-named command matches.
+
+    Returns:
+     list of tuples: [(cmdname, args, cmdobj, cmdlen, mratio), ...]
+            where cmdname is the matching command name and args is
+            everything not included in the cmdname. Cmdobj is the actual
+            command instance taken from the cmdset, cmdlen is the length
+            of the command name and the mratio is some quality value to
+            (possibly) separate multiple matches.
+
+    """
+    # Your implementation here

+ 38 - 0
server/conf/connection_screens.py

@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+"""
+Connection screen
+
+This is the text to show the user when they first connect to the game (before
+they log in).
+
+To change the login screen in this module, do one of the following:
+
+- Define a function `connection_screen()`, taking no arguments. This will be
+  called first and must return the full string to act as the connection screen.
+  This can be used to produce more dynamic screens.
+- Alternatively, define a string variable in the outermost scope of this module
+  with the connection string that should be displayed. If more than one such
+  variable is given, Evennia will pick one of them at random.
+
+The commands available to the user when the connection screen is shown
+are defined in evennia.default_cmds.UnloggedinCmdSet. The parsing and display
+of the screen is done by the unlogged-in "look" command.
+
+"""
+
+from django.conf import settings
+from evennia import utils
+
+CONNECTION_SCREEN = """
+|b==============================================================|n
+ Welcome to |g{}|n, version {}!
+
+ If you have an existing account, connect to it by typing:
+      |wconnect <username> <password>|n
+ If you need to create an account, type (without the <>'s):
+      |wcreate <username> <password>|n
+
+ Enter |whelp|n for more info. |wlook|n will re-show this screen.
+|b==============================================================|n""".format(
+    settings.SERVERNAME, utils.get_evennia_version("short")
+)

+ 51 - 0
server/conf/inlinefuncs.py

@@ -0,0 +1,51 @@
+"""
+Inlinefunc
+
+Inline functions allow for direct conversion of text users mark in a
+special way. Inlinefuncs are deactivated by default. To activate, add
+
+    INLINEFUNC_ENABLED = True
+
+to your settings file. The default inlinefuncs are found in
+evennia.utils.inlinefunc.
+
+In text, usage is straightforward:
+
+$funcname([arg1,[arg2,...]])
+
+Example 1 (using the "pad" inlinefunc):
+    say This is $pad("a center-padded text", 50,c,-) of width 50.
+    ->
+    John says, "This is -------------- a center-padded text--------------- of width 50."
+
+Example 2 (using nested "pad" and "time" inlinefuncs):
+    say The time is $pad($time(), 30)right now.
+    ->
+    John says, "The time is         Oct 25, 11:09         right now."
+
+To add more inline functions, add them to this module, using
+the following call signature:
+
+    def funcname(text, *args, **kwargs)
+
+where `text` is always the part between {funcname(args) and
+{/funcname and the *args are taken from the appropriate part of the
+call. If no {/funcname is given, `text` will be the empty string.
+
+It is important that the inline function properly clean the
+incoming `args`, checking their type and replacing them with sane
+defaults if needed. If impossible to resolve, the unmodified text
+should be returned. The inlinefunc should never cause a traceback.
+
+While the inline function should accept **kwargs, the keyword is
+never accepted as a valid call - this is only intended to be used
+internally by Evennia, notably to send the `session` keyword to
+the function; this is the session of the object viewing the string
+and can be used to customize it to each session.
+
+"""
+
+# def capitalize(text, *args, **kwargs):
+#    "Silly capitalize example. Used as {capitalize() ... {/capitalize"
+#    session = kwargs.get("session")
+#    return text.capitalize()

+ 52 - 0
server/conf/inputfuncs.py

@@ -0,0 +1,52 @@
+"""
+Input functions
+
+Input functions are always called from the client (they handle server
+input, hence the name).
+
+This module is loaded by being included in the
+`settings.INPUT_FUNC_MODULES` tuple.
+
+All *global functions* included in this module are considered
+input-handler functions and can be called by the client to handle
+input.
+
+An input function must have the following call signature:
+
+    cmdname(session, *args, **kwargs)
+
+Where session will be the active session and *args, **kwargs are extra
+incoming arguments and keyword properties.
+
+A special command is the "default" command, which is will be called
+when no other cmdname matches. It also receives the non-found cmdname
+as argument.
+
+    default(session, cmdname, *args, **kwargs)
+
+"""
+
+# def oob_echo(session, *args, **kwargs):
+#     """
+#     Example echo function. Echoes args, kwargs sent to it.
+#
+#     Args:
+#         session (Session): The Session to receive the echo.
+#         args (list of str): Echo text.
+#         kwargs (dict of str, optional): Keyed echo text
+#
+#     """
+#     session.msg(oob=("echo", args, kwargs))
+#
+#
+# def default(session, cmdname, *args, **kwargs):
+#     """
+#     Handles commands without a matching inputhandler func.
+#
+#     Args:
+#         session (Session): The active Session.
+#         cmdname (str): The (unmatched) command name
+#         args, kwargs (any): Arguments to function.
+#
+#     """
+#     pass

+ 30 - 0
server/conf/lockfuncs.py

@@ -0,0 +1,30 @@
+"""
+
+Lockfuncs
+
+Lock functions are functions available when defining lock strings,
+which in turn limits access to various game systems.
+
+All functions defined globally in this module are assumed to be
+available for use in lockstrings to determine access. See the
+Evennia documentation for more info on locks.
+
+A lock function is always called with two arguments, accessing_obj and
+accessed_obj, followed by any number of arguments. All possible
+arguments should be handled with *args, **kwargs. The lock function
+should handle all eventual tracebacks by logging the error and
+returning False.
+
+Lock functions in this module extend (and will overload same-named)
+lock functions from evennia.locks.lockfuncs.
+
+"""
+
+# def myfalse(accessing_obj, accessed_obj, *args, **kwargs):
+#    """
+#    called in lockstring with myfalse().
+#    A simple logger that always returns false. Prints to stdout
+#    for simplicity, should use utils.logger for real operation.
+#    """
+#    print "%s tried to access %s. Access denied." % (accessing_obj, accessed_obj)
+#    return False

+ 105 - 0
server/conf/mssp.py

@@ -0,0 +1,105 @@
+"""
+
+MSSP (Mud Server Status Protocol) meta information
+
+Modify this file to specify what MUD listing sites will report about your game.
+All fields are static. The number of currently active players and your game's
+current uptime will be added automatically by Evennia.
+
+You don't have to fill in everything (and most fields are not shown/used by all
+crawlers anyway); leave the default if so needed. You need to reload the server
+before the updated information is made available to crawlers (reloading does
+not affect uptime).
+
+After changing the values in this file, you must register your game with the
+MUD website list you want to track you. The listing crawler will then regularly
+connect to your server to get the latest info. No further configuration is
+needed on the Evennia side.
+
+"""
+
+MSSPTable = {
+    # Required fields
+    "NAME": "Mygame",  # usually the same as SERVERNAME
+    # Generic
+    "CRAWL DELAY": "-1",  # limit how often crawler may update the listing. -1 for no limit
+    "HOSTNAME": "",  # telnet hostname
+    "PORT": ["4000"],  # telnet port - most important port should be *last* in list!
+    "CODEBASE": "Evennia",
+    "CONTACT": "",  # email for contacting the mud
+    "CREATED": "",  # year MUD was created
+    "ICON": "",  # url to icon 32x32 or larger; <32kb.
+    "IP": "",  # current or new IP address
+    "LANGUAGE": "",  # name of language used, e.g. English
+    "LOCATION": "",  # full English name of server country
+    "MINIMUM AGE": "0",  # set to 0 if not applicable
+    "WEBSITE": "",  # http:// address to your game website
+    # Categorisation
+    "FAMILY": "Custom",  # evennia goes under 'Custom'
+    "GENRE": "None",  # Adult, Fantasy, Historical, Horror, Modern, None, or Science Fiction
+    # Gameplay: Adventure, Educational, Hack and Slash, None,
+    # Player versus Player, Player versus Environment,
+    # Roleplaying, Simulation, Social or Strategy
+    "GAMEPLAY": "",
+    "STATUS": "Open Beta",  # Allowed: Alpha, Closed Beta, Open Beta, Live
+    "GAMESYSTEM": "Custom",  # D&D, d20 System, World of Darkness, etc. Use Custom if homebrew
+    # Subgenre: LASG, Medieval Fantasy, World War II, Frankenstein,
+    # Cyberpunk, Dragonlance, etc. Or None if not applicable.
+    "SUBGENRE": "None",
+    # World
+    "AREAS": "0",
+    "HELPFILES": "0",
+    "MOBILES": "0",
+    "OBJECTS": "0",
+    "ROOMS": "0",  # use 0 if room-less
+    "CLASSES": "0",  # use 0 if class-less
+    "LEVELS": "0",  # use 0 if level-less
+    "RACES": "0",  # use 0 if race-less
+    "SKILLS": "0",  # use 0 if skill-less
+    # Protocols set to 1 or 0; should usually not be changed)
+    "ANSI": "1",
+    "GMCP": "1",
+    "MSDP": "1",
+    "MXP": "1",
+    "SSL": "1",
+    "UTF-8": "1",
+    "MCCP": "1",
+    "XTERM 256 COLORS": "1",
+    "XTERM TRUE COLORS": "0",
+    "ATCP": "0",
+    "MCP": "0",
+    "MSP": "0",
+    "VT100": "0",
+    "PUEBLO": "0",
+    "ZMP": "0",
+    # Commercial set to 1 or 0)
+    "PAY TO PLAY": "0",
+    "PAY FOR PERKS": "0",
+    # Hiring  set to 1 or 0)
+    "HIRING BUILDERS": "0",
+    "HIRING CODERS": "0",
+    # Extended variables
+    # World
+    "DBSIZE": "0",
+    "EXITS": "0",
+    "EXTRA DESCRIPTIONS": "0",
+    "MUDPROGS": "0",
+    "MUDTRIGS": "0",
+    "RESETS": "0",
+    # Game  (set to 1 or 0, or one of the given alternatives)
+    "ADULT MATERIAL": "0",
+    "MULTICLASSING": "0",
+    "NEWBIE FRIENDLY": "0",
+    "PLAYER CITIES": "0",
+    "PLAYER CLANS": "0",
+    "PLAYER CRAFTING": "0",
+    "PLAYER GUILDS": "0",
+    "EQUIPMENT SYSTEM": "None",  # "None", "Level", "Skill", "Both"
+    "MULTIPLAYING": "None",  # "None", "Restricted", "Full"
+    "PLAYERKILLING": "None",  # "None", "Restricted", "Full"
+    "QUEST SYSTEM": "None",  # "None", "Immortal Run", "Automated", "Integrated"
+    "ROLEPLAYING": "None",  # "None", "Accepted", "Encouraged", "Enforced"
+    "TRAINING SYSTEM": "None",  # "None", "Level", "Skill", "Both"
+    # World originality: "All Stock", "Mostly Stock", "Mostly Original", "All Original"
+    "WORLD ORIGINALITY": "All Original",
+}

+ 24 - 0
server/conf/portal_services_plugins.py

@@ -0,0 +1,24 @@
+"""
+Start plugin services
+
+This plugin module can define user-created services for the Portal to
+start.
+
+This module must handle all imports and setups required to start
+twisted services (see examples in evennia.server.portal.portal). It
+must also contain a function start_plugin_services(application).
+Evennia will call this function with the main Portal application (so
+your services can be added to it). The function should not return
+anything. Plugin services are started last in the Portal startup
+process.
+
+"""
+
+
+def start_plugin_services(portal):
+    """
+    This hook is called by Evennia, last in the Portal startup process.
+
+    portal - a reference to the main portal application.
+    """
+    pass

+ 24 - 0
server/conf/server_services_plugins.py

@@ -0,0 +1,24 @@
+"""
+
+Server plugin services
+
+This plugin module can define user-created services for the Server to
+start.
+
+This module must handle all imports and setups required to start a
+twisted service (see examples in evennia.server.server). It must also
+contain a function start_plugin_services(application). Evennia will
+call this function with the main Server application (so your services
+can be added to it). The function should not return anything. Plugin
+services are started last in the Server startup process.
+
+"""
+
+
+def start_plugin_services(server):
+    """
+    This hook is called by Evennia, last in the Server startup process.
+
+    server - a reference to the main server application.
+    """
+    pass

+ 37 - 0
server/conf/serversession.py

@@ -0,0 +1,37 @@
+"""
+ServerSession
+
+The serversession is the Server-side in-memory representation of a
+user connecting to the game.  Evennia manages one Session per
+connection to the game. So a user logged into the game with multiple
+clients (if Evennia is configured to allow that) will have multiple
+sessions tied to one Account object. All communication between Evennia
+and the real-world user goes through the Session(s) associated with that user.
+
+It should be noted that modifying the Session object is not usually
+necessary except for the most custom and exotic designs - and even
+then it might be enough to just add custom session-level commands to
+the SessionCmdSet instead.
+
+This module is not normally called. To tell Evennia to use the class
+in this module instead of the default one, add the following to your
+settings file:
+
+    SERVER_SESSION_CLASS = "server.conf.serversession.ServerSession"
+
+"""
+
+from evennia.server.serversession import ServerSession as BaseServerSession
+
+
+class ServerSession(BaseServerSession):
+    """
+    This class represents a player's session and is a template for
+    individual protocols to communicate with Evennia.
+
+    Each account gets one or more sessions assigned to them whenever they connect
+    to the game server. All communication between game and account goes
+    through their session(s).
+    """
+
+    pass

+ 50 - 0
server/conf/settings.py

@@ -0,0 +1,50 @@
+r"""
+Evennia settings file.
+
+The available options are found in the default settings file found
+here:
+
+/home/cek/workspace/DKmud/evennia/evennia/settings_default.py
+
+Remember:
+
+Don't copy more from the default file than you actually intend to
+change; this will make sure that you don't overload upstream updates
+unnecessarily.
+
+When changing a setting requiring a file system path (like
+path/to/actual/file.py), use GAME_DIR and EVENNIA_DIR to reference
+your game folder and the Evennia library folders respectively. Python
+paths (path.to.module) should be given relative to the game's root
+folder (typeclasses.foo) whereas paths within the Evennia library
+needs to be given explicitly (evennia.foo).
+
+If you want to share your game dir, including its settings, you can
+put secret game- or server-specific settings in secret_settings.py.
+
+"""
+
+# Use the defaults from Evennia unless explicitly overridden
+from evennia.settings_default import *
+
+######################################################################
+# Evennia base server config
+######################################################################
+
+# This is the name of your game. Make it catchy!
+SERVERNAME = "dkmud"
+
+GLOBAL_SCRIPTS = {
+    "__ai_manager__": {'typeclass': 'typeclasses.scripts.AiManagerScript',
+             'repeats': 0, 'interval': 1}
+}
+
+CRAFT_RECIPE_MODULES = ['world.recipes_base']
+
+######################################################################
+# Settings given in secret_settings.py override those in this file.
+######################################################################
+try:
+    from server.conf.secret_settings import *
+except ImportError:
+    print("secret_settings.py file not found or failed to import.")

+ 41 - 0
server/conf/web_plugins.py

@@ -0,0 +1,41 @@
+"""
+Web plugin hooks.
+"""
+
+
+def at_webserver_root_creation(web_root):
+    """
+    This is called as the web server has finished building its default
+    path tree. At this point, the media/ and static/ URIs have already
+    been added to the web root.
+
+    Args:
+        web_root (twisted.web.resource.Resource): The root
+            resource of the URI tree. Use .putChild() to
+            add new subdomains to the tree.
+
+    Returns:
+        web_root (twisted.web.resource.Resource): The potentially
+            modified root structure.
+
+    Example:
+        from twisted.web import static
+        my_page = static.File("web/mypage/")
+        my_page.indexNames = ["index.html"]
+        web_root.putChild("mypage", my_page)
+
+    """
+    return web_root
+
+
+def at_webproxy_root_creation(web_root):
+    """
+    This function can modify the portal proxy service.
+    Args:
+        web_root (evennia.server.webserver.Website): The Evennia
+            Website application. Use .putChild() to add new
+            subdomains that are Portal-accessible over TCP;
+            primarily for new protocol development, but suitable
+            for other shenanigans.
+    """
+    return web_root

+ 15 - 0
server/logs/README.md

@@ -0,0 +1,15 @@
+This directory contains Evennia's log files. The existence of this README.md file is also necessary
+to correctly include the log directory in git (since log files are ignored by git and you can't
+commit an empty directory). 
+
+- `server.log` - log file from the game Server.
+- `portal.log` - log file from Portal proxy (internet facing)
+
+Usually these logs are viewed together with `evennia -l`. They are also rotated every week so as not
+to be too big. Older log names will have a name appended by `_month_date`. 
+ 
+- `lockwarnings.log` - warnings from the lock system.
+- `http_requests.log` - this will generally be empty unless turning on debugging inside the server.
+
+- `channel_<channelname>.log` - these are channel logs for the in-game channels They are also used
+  by the `/history` flag in-game to get the latest message history. 

+ 15 - 0
typeclasses/README.md

@@ -0,0 +1,15 @@
+# typeclasses/
+
+This directory holds the modules for overloading all the typeclasses
+representing the game entities and many systems of the game. Other
+server functionality not covered here is usually modified by the
+modules in `server/conf/`.
+
+Each module holds empty classes that just imports Evennia's defaults.
+Any modifications done to these classes will overload the defaults.
+
+You can change the structure of this directory (even rename the
+directory itself) as you please, but if you do you must add the
+appropriate new paths to your settings.py file so Evennia knows where
+to look. Also remember that for Python to find your modules, it
+requires you to add an empty `__init__.py` file in any new subdirectories you create.

+ 0 - 0
typeclasses/__init__.py


+ 104 - 0
typeclasses/accounts.py

@@ -0,0 +1,104 @@
+"""
+Account
+
+The Account represents the game "account" and each login has only one
+Account object. An Account is what chats on default channels but has no
+other in-game-world existence. Rather the Account puppets Objects (such
+as Characters) in order to actually participate in the game world.
+
+
+Guest
+
+Guest accounts are simple low-level accounts that are created/deleted
+on the fly and allows users to test the game without the commitment
+of a full registration. Guest accounts are deactivated by default; to
+activate them, add the following line to your settings file:
+
+    GUEST_ENABLED = True
+
+You will also need to modify the connection screen to reflect the
+possibility to connect with a guest account. The setting file accepts
+several more options for customizing the Guest account system.
+
+"""
+
+from evennia import DefaultAccount, DefaultGuest
+
+
+class Account(DefaultAccount):
+    """
+    This class describes the actual OOC account (i.e. the user connecting
+    to the MUD). It does NOT have visual appearance in the game world (that
+    is handled by the character which is connected to this). Comm channels
+    are attended/joined using this object.
+
+    It can be useful e.g. for storing configuration options for your game, but
+    should generally not hold any character-related info (that's best handled
+    on the character level).
+
+    Can be set using BASE_ACCOUNT_TYPECLASS.
+
+
+    * available properties
+
+     key (string) - name of account
+     name (string)- wrapper for user.username
+     aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings.
+     dbref (int, read-only) - unique #id-number. Also "id" can be used.
+     date_created (string) - time stamp of object creation
+     permissions (list of strings) - list of permission strings
+
+     user (User, read-only) - django User authorization object
+     obj (Object) - game object controlled by account. 'character' can also be used.
+     sessions (list of Sessions) - sessions connected to this account
+     is_superuser (bool, read-only) - if the connected user is a superuser
+
+    * Handlers
+
+     locks - lock-handler: use locks.add() to add new lock strings
+     db - attribute-handler: store/retrieve database attributes on this self.db.myattr=val, val=self.db.myattr
+     ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data
+     scripts - script-handler. Add new scripts to object with scripts.add()
+     cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object
+     nicks - nick-handler. New nicks with nicks.add().
+
+    * Helper methods
+
+     msg(text=None, **kwargs)
+     execute_cmd(raw_string, session=None)
+     search(ostring, global_search=False, attribute_name=None, use_nicks=False, location=None, ignore_errors=False, account=False)
+     is_typeclass(typeclass, exact=False)
+     swap_typeclass(new_typeclass, clean_attributes=False, no_default=True)
+     access(accessing_obj, access_type='read', default=False)
+     check_permstring(permstring)
+
+    * Hook methods (when re-implementation, remember methods need to have self as first arg)
+
+     basetype_setup()
+     at_account_creation()
+
+     - note that the following hooks are also found on Objects and are
+       usually handled on the character level:
+
+     at_init()
+     at_cmdset_get(**kwargs)
+     at_first_login()
+     at_post_login(session=None)
+     at_disconnect()
+     at_message_receive()
+     at_message_send()
+     at_server_reload()
+     at_server_shutdown()
+
+    """
+
+    pass
+
+
+class Guest(DefaultGuest):
+    """
+    This class is used for guest logins. Unlike Accounts, Guests and their
+    characters are deleted after disconnection.
+    """
+
+    pass

+ 62 - 0
typeclasses/channels.py

@@ -0,0 +1,62 @@
+"""
+Channel
+
+The channel class represents the out-of-character chat-room usable by
+Accounts in-game. It is mostly overloaded to change its appearance, but
+channels can be used to implement many different forms of message
+distribution systems.
+
+Note that sending data to channels are handled via the CMD_CHANNEL
+syscommand (see evennia.syscmds). The sending should normally not need
+to be modified.
+
+"""
+
+from evennia import DefaultChannel
+
+
+class Channel(DefaultChannel):
+    """
+    Working methods:
+        at_channel_creation() - called once, when the channel is created
+        has_connection(account) - check if the given account listens to this channel
+        connect(account) - connect account to this channel
+        disconnect(account) - disconnect account from channel
+        access(access_obj, access_type='listen', default=False) - check the
+                    access on this channel (default access_type is listen)
+        delete() - delete this channel
+        message_transform(msg, emit=False, prefix=True,
+                          sender_strings=None, external=False) - called by
+                          the comm system and triggers the hooks below
+        msg(msgobj, header=None, senders=None, sender_strings=None,
+            persistent=None, online=False, emit=False, external=False) - main
+                send method, builds and sends a new message to channel.
+        tempmsg(msg, header=None, senders=None) - wrapper for sending non-persistent
+                messages.
+        distribute_message(msg, online=False) - send a message to all
+                connected accounts on channel, optionally sending only
+                to accounts that are currently online (optimized for very large sends)
+
+    Useful hooks:
+        channel_prefix(msg, emit=False) - how the channel should be
+                  prefixed when returning to user. Returns a string
+        format_senders(senders) - should return how to display multiple
+                senders to a channel
+        pose_transform(msg, sender_string) - should detect if the
+                sender is posing, and if so, modify the string
+        format_external(msg, senders, emit=False) - format messages sent
+                from outside the game, like from IRC
+        format_message(msg, emit=False) - format the message body before
+                displaying it to the user. 'emit' generally means that the
+                message should not be displayed with the sender's name.
+
+        pre_join_channel(joiner) - if returning False, abort join
+        post_join_channel(joiner) - called right after successful join
+        pre_leave_channel(leaver) - if returning False, abort leave
+        post_leave_channel(leaver) - called right after successful leave
+        pre_send_message(msg) - runs just before a message is sent to channel
+        post_send_message(msg) - called just after message was sent to channel
+
+    """
+
+    pass

+ 80 - 0
typeclasses/characters.py

@@ -0,0 +1,80 @@
+"""
+Characters
+
+Characters are (by default) Objects setup to be puppeted by Accounts.
+They are what you "see" in game. The Character class in this module
+is setup to be the "default" character type created by the default
+creation commands.
+
+"""
+from evennia import DefaultCharacter
+from evennia.utils import inherits_from
+
+from typeclasses import rooms
+from typeclasses.exits import Exit
+from utils.utils import has_tag, has_effect, has_effect_in
+
+
+class Character(DefaultCharacter):
+    """
+    The Character defaults to reimplementing some of base Object's hook methods with the
+    following functionality:
+
+    at_basetype_setup - always assigns the DefaultCmdSet to this object type
+                    (important!)sets locks so character cannot be picked up
+                    and its commands only be called by itself, not anyone else.
+                    (to change things, use at_object_creation() instead).
+    at_after_move(source_location) - Launches the "look" command after every move.
+    at_post_unpuppet(account) -  when Account disconnects from the Character, we
+                    store the current location in the pre_logout_location Attribute and
+                    move it to a None-location so the "unpuppeted" character
+                    object does not need to stay on grid. Echoes "Account has disconnected"
+                    to the room.
+    at_pre_puppet - Just before Account re-connects, retrieves the character's
+                    pre_logout_location Attribute and move it back on the grid.
+    at_post_puppet - Echoes "AccountName has entered the game" to the room.
+
+    """
+
+    def at_object_creation(self):
+        self.db.desc = "A human being."
+
+        self.db.health = 1
+        self.db.mana = 1
+
+        self.db.strength = 1
+        self.db.agility = 1
+        self.db.intellect = 1
+
+        self.db.equipment = {
+            'head': None,
+            'torso': None,
+            'legs': None,
+            'right hand': None,
+            'left hand': None,
+            'foot': None
+        }
+
+        self.db.spells = []
+        self.db.current_action = None
+
+    def get_health(self):
+        return self.db.health
+
+    def get_mana(self):
+        return self.db.mana
+
+    def get_abilities(self):
+        return self.db.strength, self.db.agility, self.db.intellect
+
+    def at_look(self, target, **kwargs):
+        description = super().at_look(target, **kwargs)
+
+        # You can't see things in room if it's dark.
+        if inherits_from(self.location, rooms.IndoorRoom):
+            if not self.is_superuser and not self.location.db.is_lit and \
+                    not (inherits_from(target, rooms.Room) or inherits_from(target, Exit)):
+                description = "Could not find '{}'.".format(target.name)
+
+        return description
+

+ 81 - 0
typeclasses/effects.py

@@ -0,0 +1,81 @@
+from evennia.utils import inherits_from, logger
+
+from typeclasses.characters import Character
+from typeclasses.scripts import Script
+from typeclasses.rooms import IndoorRoom
+from utils.utils import has_tag, toggle_effect, has_effect
+
+
+class EffectMagicalLight(Script):
+    """
+    """
+    def at_script_creation(self):
+        self.key = "effect_magic_light_script"
+        self.desc = "not now"
+        self.start_delay = True
+        self.interval = 20
+        self.persistent = True  # will survive reload
+        self.repeats = 1
+
+    def at_start(self):
+        if self.obj:
+            if not has_effect(self.obj, "emit_magic_light"):
+                toggle_effect(self.obj, "emit_magic_light")
+
+                if self.obj.location:
+                    if inherits_from(self.obj.location, IndoorRoom):
+                        self.obj.location.msg_contents("{} starts emitting a soft and steady light.".format(self.obj.name))
+                        self.obj.location.check_light_state()
+                    # check if effect target is in actor contents
+                    if self.obj.location.location and inherits_from(self.obj.location.location, IndoorRoom):
+                        if inherits_from(self.obj.location, Character):
+                            self.obj.location.msg("{} starts emitting a soft and steady light.".format(self.obj.name))
+                        self.obj.location.location.check_light_state()
+
+    def at_stop(self):
+        if self.obj:
+            if has_effect(self.obj, "emit_magic_light"):
+                toggle_effect(self.obj, "emit_magic_light")
+
+                if self.obj.location:
+                    if inherits_from(self.obj.location, IndoorRoom):
+                        self.obj.location.msg_contents("{} stops emitting light.".format(self.obj.name))
+                        self.obj.location.check_light_state()
+                    # check if effect target is in actor contents
+                    if self.obj.location.location and inherits_from(self.obj.location.location, IndoorRoom):
+                        if inherits_from(self.obj.location, Character):
+                            self.obj.location.msg("{} stops emitting light.".format(self.obj.name))
+                        self.obj.location.location.check_light_state()
+
+    def at_repeat(self):
+        self.at_stop()
+
+
+class EffectCharm(Script):
+    """
+    """
+
+    def at_script_creation(self):
+        self.key = "effect_charm_script"
+        self.desc = "not now"
+        self.start_delay = True
+        self.interval = 20
+        self.persistent = True  # will survive reload
+        self.repeats = 1
+
+    def at_start(self):
+        if self.obj:
+            if not has_effect(self.obj, "charm"):
+                toggle_effect(self.obj, "charm")
+                self.obj.db.real_owner = self.obj.db.owner
+                self.obj.db.owner = self.db.source
+
+    def at_stop(self):
+        if self.obj:
+            if has_effect(self.obj, "charm"):
+                toggle_effect(self.obj, "charm")
+                self.obj.db.owner = self.obj.db.real_owner
+                del self.obj.db.real_owner
+
+    def at_repeat(self):
+        self.at_stop()

+ 171 - 0
typeclasses/exits.py

@@ -0,0 +1,171 @@
+"""
+Exits
+
+Exits are connectors between Rooms. An exit always has a destination property
+set and has a single command defined on itself with the same name as its key,
+for allowing Characters to traverse the exit to its destination.
+
+"""
+import random
+
+from evennia import DefaultExit
+
+from utils.utils import has_effect
+
+
+class Exit(DefaultExit):
+    """
+    Exits are connectors between rooms. Exits are normal Objects except
+    they defines the `destination` property. It also does work in the
+    following methods:
+
+     basetype_setup() - sets default exit locks (to change, use `at_object_creation` instead).
+     at_cmdset_get(**kwargs) - this is called when the cmdset is accessed and should
+                              rebuild the Exit cmdset along with a command matching the name
+                              of the Exit object. Conventionally, a kwarg `force_init`
+                              should force a rebuild of the cmdset, this is triggered
+                              by the `@alias` command when aliases are changed.
+     at_failed_traverse() - gives a default error message ("You cannot
+                            go there") if exit traversal fails and an
+                            attribute `err_traverse` is not defined.
+
+    Relevant hooks to overload (compared to other types of Objects):
+        at_traverse(traveller, target_loc) - called to do the actual traversal and calling of the other hooks.
+                                            If overloading this, consider using super() to use the default
+                                            movement implementation (and hook-calling).
+        at_after_traverse(traveller, source_loc) - called by at_traverse just after traversing.
+        at_failed_traverse(traveller) - called by at_traverse if traversal failed for some reason. Will
+                                        not be called if the attribute `err_traverse` is
+                                        defined, in which case that will simply be echoed.
+    """
+
+    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()))
+        else:
+            super().at_traverse(traversing_object, target_location, **kwargs)
+
+    def delete(self):
+        if self.location and self.location.db.zone:
+            self.location.db.zone.remove_room_exit(self)
+        return super().delete()
+
+"""
+BaseDoor
+
+Contribution - Griatch 2016
+
+A simple two-way exit that represents a door that can be opened and
+closed. Can easily be expanded from to make it lockable, destroyable
+etc.
+To try it out, `@dig` a new room and then use the (overloaded) `@open`
+command to open a new doorway to it like this:
+
+    @open doorway:contrib.simpledoor.SimpleDoor = otherroom
+
+You can then use `open doorway' and `close doorway` to change the open
+state. If you are not superuser (`@quell` yourself) you'll find you
+cannot pass through either side of the door once it's closed from the
+other side.
+
+"""
+
+class BaseDoor(Exit):
+    """
+    A two-way exit "door" with some methods for affecting both "sides"
+    of the door at the same time. For example, set a lock on either of the two
+    sides using `exitname.setlock("traverse:false())`
+
+    """
+
+    DARK_MESSAGES = (
+        "It is pitch black. You are likely to be eaten by a grue.",
+        "It's pitch black. You fumble around but cannot find anything.",
+        "You don't see a thing. You feel around, managing to bump your fingers hard against something. Ouch!",
+        "You don't see a thing! Blindly grasping the air around you, you find nothing.",
+        "It's totally dark here. You almost stumble over some un-evenness in the ground.",
+        "You are completely blind. For a moment you think you hear someone breathing nearby ... "
+        "\n ... surely you must be mistaken.",
+        "Blind, you think you find some sort of object on the ground, but it turns out to be just a stone.",
+        "Blind, you bump into a wall. The wall seems to be covered with some sort of vegetation,"
+        " but its too damp to burn.",
+        "You can't see anything, but the air is damp. It feels like you are far underground.",
+    )
+
+    def at_object_creation(self):
+        """
+        Called the very first time the door is created.
+
+        """
+        self.db.dark_desc = ""
+        self.db.return_exit = None
+
+    def setlock(self, lockstring):
+        """
+        Sets identical locks on both sides of the door.
+
+        Args:
+            lockstring (str): A lockstring, like `"traverse:true()"`.
+
+        """
+        self.locks.add(lockstring)
+        self.db.return_exit.locks.add(lockstring)
+
+    def setdesc(self, description):
+        """
+        Sets identical descs on both sides of the door.
+
+        Args:
+            setdesc (str): A description.
+
+        """
+        self.db.desc = description
+        self.db.return_exit.db.desc = description
+
+    def delete(self):
+        """
+        Deletes both sides of the door.
+
+        """
+        # we have to be careful to avoid a delete-loop.
+        if self.db.return_exit:
+            super().delete()
+        super().delete()
+        return True
+
+    def at_failed_traverse(self, traversing_object):
+        """
+        Called when door traverse: lock fails.
+
+        Args:
+            traversing_object (Typeclassed entity): The object
+                attempting the traversal.
+
+        """
+        traversing_object.msg("%s is closed." % self.key)
+
+    def return_appearance(self, looker, **kwargs):
+        """
+        This formats a description. It is the hook a 'look' command
+        should call.
+
+        Args:
+            looker (Object): Object doing the looking.
+            **kwargs (dict): Arbitrary, optional arguments for users
+                overriding the call (unused by default).
+        """
+        if not looker:
+            return ""
+
+        string = "{}\n".format(self.get_display_name(looker))
+        string += "-" * 100
+        string += "|/"
+
+        if not self.db.is_lit and not looker.is_superuser:
+            desc = self.db.dark_desc if self.db.dark_desc else random.choice(self.DARK_MESSAGES)
+        else:
+            desc = self.db.desc
+        if desc:
+            string += "{}".format(desc)
+
+        return string

+ 41 - 0
typeclasses/mob_actions.py

@@ -0,0 +1,41 @@
+import random
+from evennia import gametime
+
+from typeclasses.objects import Object
+
+class Action(Object):
+    def at_object_creation(self):
+        super().at_object_creation()
+
+        self.db.action_time = 0
+        self.db.action_completion_time = 0
+
+    def prepare(self, actor):
+        #set duration of action
+        self.db.action_completion_time = gametime.gametime() + self.db.action_time
+
+    def update(self, actor):
+        pass
+
+    def complete(self, actor):
+        pass
+
+    def completion_time(self):
+        return self.db.action_completion_time
+
+class ActionIdle(Action):
+    def at_object_creation(self):
+        super().at_object_creation()
+
+        self.db.action_time = 10
+
+    def update(self, actor):
+        pass
+
+    def complete(self, actor):
+        roll = random.randrange(100)
+        if roll < 10:
+            actor.emote()
+
+        #TEST
+        actor.db.energy = 0 if actor.db.energy == 0 else actor.db.energy - 1

+ 47 - 0
typeclasses/mobs.py

@@ -0,0 +1,47 @@
+import random
+
+from evennia import create_object
+
+from typeclasses.objects import Object
+from typeclasses.scripts import Script
+from typeclasses.mob_actions import ActionIdle
+
+class Mob(Object):
+    def at_object_creation(self):
+        super().at_object_creation()
+
+        self.tags.add("ai_mob", category="general")
+        self.db.owner = None
+
+        self.db.health = 1
+        self.db.mana = 1
+
+        self.db.strength = 1
+        self.db.agility = 1
+        self.db.intellect = 1
+
+        # needs
+        self.db.energy = 100
+
+    def at_object_delete(self):
+        if self.db.action:
+            self.db.action.delete()
+
+        return True
+
+    def at_init(self):
+        self.db.action = None
+
+    def tick(self):
+        pass
+
+    def think(self):
+
+
+        if not self.db.action:
+            self.db.action = create_object(ActionIdle, key="action_idle")
+            self.db.action.prepare(self)
+
+    def emote(self):
+        if self.location:
+            self.location.msg_contents("{} is thinking something.".format(self.get_display_name(self.location)), exclude=self, from_obj=self)

+ 264 - 0
typeclasses/objects.py

@@ -0,0 +1,264 @@
+"""
+Object
+
+The Object is the "naked" base class for things in the game world.
+
+Note that the default Character, Room and Exit does not inherit from
+this Object, but from their respective default implementations in the
+evennia library. If you want to use this class as a parent to change
+the other types, you can do so by adding this as a multiple
+inheritance.
+
+"""
+from collections import defaultdict
+
+from evennia import DefaultObject
+from evennia.utils import logger, evtable, inherits_from
+
+from typeclasses.rooms import IndoorRoom
+from utils.utils import has_effect_in, has_tag
+
+
+class Object(DefaultObject):
+    """
+    This is the root typeclass object, implementing an in-game Evennia
+    game object, such as having a location, being able to be
+    manipulated or looked at, etc. If you create a new typeclass, it
+    must always inherit from this object (or any of the other objects
+    in this file, since they all actually inherit from BaseObject, as
+    seen in src.object.objects).
+
+    The BaseObject class implements several hooks tying into the game
+    engine. By re-implementing these hooks you can control the
+    system. You should never need to re-implement special Python
+    methods, such as __init__ and especially never __getattribute__ and
+    __setattr__ since these are used heavily by the typeclass system
+    of Evennia and messing with them might well break things for you.
+
+
+    * Base properties defined/available on all Objects
+
+     key (string) - name of object
+     name (string)- same as key
+     dbref (int, read-only) - unique #id-number. Also "id" can be used.
+     date_created (string) - time stamp of object creation
+
+     account (Account) - controlling account (if any, only set together with
+                       sessid below)
+     sessid (int, read-only) - session id (if any, only set together with
+                       account above). Use `sessions` handler to get the
+                       Sessions directly.
+     location (Object) - current location. Is None if this is a room
+     home (Object) - safety start-location
+     has_account (bool, read-only)- will only return *connected* accounts
+     contents (list of Objects, read-only) - returns all objects inside this
+                       object (including exits)
+     exits (list of Objects, read-only) - returns all exits from this
+                       object, if any
+     destination (Object) - only set if this object is an exit.
+     is_superuser (bool, read-only) - True/False if this user is a superuser
+
+    * Handlers available
+
+     aliases - alias-handler: use aliases.add/remove/get() to use.
+     permissions - permission-handler: use permissions.add/remove() to
+                   add/remove new perms.
+     locks - lock-handler: use locks.add() to add new lock strings
+     scripts - script-handler. Add new scripts to object with scripts.add()
+     cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object
+     nicks - nick-handler. New nicks with nicks.add().
+     sessions - sessions-handler. Get Sessions connected to this
+                object with sessions.get()
+     attributes - attribute-handler. Use attributes.add/remove/get.
+     db - attribute-handler: Shortcut for attribute-handler. Store/retrieve
+            database attributes using self.db.myattr=val, val=self.db.myattr
+     ndb - non-persistent attribute handler: same as db but does not create
+            a database entry when storing data
+
+    * Helper methods (see src.objects.objects.py for full headers)
+
+     search(ostring, global_search=False, attribute_name=None,
+             use_nicks=False, location=None, ignore_errors=False, account=False)
+     execute_cmd(raw_string)
+     msg(text=None, **kwargs)
+     msg_contents(message, exclude=None, from_obj=None, **kwargs)
+     move_to(destination, quiet=False, emit_to_obj=None, use_destination=True)
+     copy(new_key=None)
+     delete()
+     is_typeclass(typeclass, exact=False)
+     swap_typeclass(new_typeclass, clean_attributes=False, no_default=True)
+     access(accessing_obj, access_type='read', default=False)
+     check_permstring(permstring)
+
+    * Hooks (these are class methods, so args should start with self):
+
+     basetype_setup()     - only called once, used for behind-the-scenes
+                            setup. Normally not modified.
+     basetype_posthook_setup() - customization in basetype, after the object
+                            has been created; Normally not modified.
+
+     at_object_creation() - only called once, when object is first created.
+                            Object customizations go here.
+     at_object_delete() - called just before deleting an object. If returning
+                            False, deletion is aborted. Note that all objects
+                            inside a deleted object are automatically moved
+                            to their <home>, they don't need to be removed here.
+
+     at_init()            - called whenever typeclass is cached from memory,
+                            at least once every server restart/reload
+     at_cmdset_get(**kwargs) - this is called just before the command handler
+                            requests a cmdset from this object. The kwargs are
+                            not normally used unless the cmdset is created
+                            dynamically (see e.g. Exits).
+     at_pre_puppet(account)- (account-controlled objects only) called just
+                            before puppeting
+     at_post_puppet()     - (account-controlled objects only) called just
+                            after completing connection account<->object
+     at_pre_unpuppet()    - (account-controlled objects only) called just
+                            before un-puppeting
+     at_post_unpuppet(account) - (account-controlled objects only) called just
+                            after disconnecting account<->object link
+     at_server_reload()   - called before server is reloaded
+     at_server_shutdown() - called just before server is fully shut down
+
+     at_access(result, accessing_obj, access_type) - called with the result
+                            of a lock access check on this object. Return value
+                            does not affect check result.
+
+     at_before_move(destination)             - called just before moving object
+                        to the destination. If returns False, move is cancelled.
+     announce_move_from(destination)         - called in old location, just
+                        before move, if obj.move_to() has quiet=False
+     announce_move_to(source_location)       - called in new location, just
+                        after move, if obj.move_to() has quiet=False
+     at_after_move(source_location)          - always called after a move has
+                        been successfully performed.
+     at_object_leave(obj, target_location)   - called when an object leaves
+                        this object in any fashion
+     at_object_receive(obj, source_location) - called when this object receives
+                        another object
+
+     at_traverse(traversing_object, source_loc) - (exit-objects only)
+                              handles all moving across the exit, including
+                              calling the other exit hooks. Use super() to retain
+                              the default functionality.
+     at_after_traverse(traversing_object, source_location) - (exit-objects only)
+                              called just after a traversal has happened.
+     at_failed_traverse(traversing_object)      - (exit-objects only) called if
+                       traversal fails and property err_traverse is not defined.
+
+     at_msg_receive(self, msg, from_obj=None, **kwargs) - called when a message
+                             (via self.msg()) is sent to this obj.
+                             If returns false, aborts send.
+     at_msg_send(self, msg, to_obj=None, **kwargs) - called when this objects
+                             sends a message to someone via self.msg().
+
+     return_appearance(looker) - describes this object. Used by "look"
+                                 command by default
+     at_desc(looker=None)      - called by 'look' whenever the
+                                 appearance is requested.
+     at_get(getter)            - called after object has been picked up.
+                                 Does not stop pickup.
+     at_drop(dropper)          - called when this object has been dropped.
+     at_say(speaker, message)  - by default, called if an object inside this
+                                 object speaks
+
+     """
+    pass
+
+
+class Feature(Object):
+    def at_object_creation(self):
+        self.locks.add("get:false(); puppet:false()")
+
+        # add attribute to store object description when viewed from a location
+        self.db.feature_desc = "You see |w{}|n.".format(self.get_numbered_name(1, None)[0])
+
+    def return_appearance(self, looker, **kwargs):
+        if not looker:
+            return ""
+        # get description, build string
+        string = "{}\n".format(self.get_display_name(looker))
+        string += "-" * 100
+        string += "|/"
+
+        desc = self.db.desc
+        if desc:
+            string += "{}".format(desc)
+
+        return string
+
+
+class ContainerFeature(Feature):
+    def at_object_creation(self):
+        super().at_object_creation()
+        self.locks.add("put:all()")
+
+    def return_appearance(self, looker, **kwargs):
+        string = super().return_appearance(looker)
+
+        # get and identify all objects
+        visible = (con for con in self.contents if con != looker and con.access(looker, "view"))
+        exits, users, things = [], [], defaultdict(list)
+        for con in visible:
+            key = con.get_display_name(looker)
+            if con.destination or con.has_account:
+                logger.log_warn("{} is an exit or a character inside container {}.".format(con.dbref, self.dbref))
+            else:
+                # things can be pluralized
+                things[key].append(con)
+
+        if things:
+            table = evtable.EvTable()
+            for key, itemlist in sorted(things.items()):
+                table.add_row("|w{}|n".format(key), "|c{}|n".format(len(itemlist)))
+
+            string += "|/Contains:|/" + str(table)
+        else:
+            string += "|/The {} is empty.".format(self.name)
+
+        return string
+
+    def at_object_receive(self, obj, source_location):
+        """
+        Called when an object enters the container.
+        """
+        if inherits_from(self.location, "typeclasses.rooms.IndoorRoom"):
+            # if we are storing a light emitting object in a container
+            # we also check the room light state.
+            if has_effect_in(obj, ['emit_magic_light', 'emit_light']):
+                self.location.check_light_state()
+
+
+class Item(Object):
+    def at_object_creation(self):
+        pass
+
+    def at_before_get(self, caller):
+        if not self.access(caller, 'view') and not caller.is_superuser:
+            caller.msg("Could not find '{}'".format(self.name))
+            return False
+
+        return True
+
+
+class EquippableItem(Item):
+    def at_object_creation(self):
+        self.locks.add("equip:all()")
+        self.db.slot = 'hand'
+
+    def at_before_drop(self, dropper, **kwargs):
+        result = super().at_before_drop(dropper, **kwargs)
+
+        if result:
+            if has_tag(self, "equipped", "general"):
+                dropper.msg("You cannot drop an equipped item.")
+                result = False
+
+        return result
+
+    def at_equip(self, caller, where, **kwargs):
+        return True
+
+    def at_unequip(self, caller, where, **kwargs):
+        return True

+ 287 - 0
typeclasses/rooms.py

@@ -0,0 +1,287 @@
+"""
+Room
+
+Rooms are simple containers that has no location of their own.
+
+"""
+from collections import defaultdict
+import random
+
+from evennia import logger, search_tag
+from evennia import DefaultRoom
+from evennia.utils.utils import list_to_string
+from evennia.utils import inherits_from
+
+from utils import spath
+from utils.utils import has_tag, fmt_light, fmt_dark, has_effect
+from typeclasses.characters import Character
+
+MAP_SIZE = 128
+
+
+class Room(DefaultRoom):
+    """
+    Rooms are like any Object, except their location is None
+    (which is default). They also use basetype_setup() to
+    add locks so they cannot be puppeted or picked up.
+    (to change that, use at_object_creation instead)
+
+    See examples/object.py for a list of
+    properties and methods available on all Objects.
+    """
+
+    def at_object_creation(self):
+        self.locks.add("light:false()")
+
+        self.db.x = 0
+        self.db.y = 0
+
+        self.db.zone = None
+
+
+class IndoorRoom(Room):
+    DARK_MESSAGES = (
+        "It is pitch black. You are likely to be eaten by a grue.",
+        "It's pitch black. You fumble around but cannot find anything.",
+        "You don't see a thing. You feel around, managing to bump your fingers hard against something. Ouch!",
+        "You don't see a thing! Blindly grasping the air around you, you find nothing.",
+        "It's totally dark here. You almost stumble over some un-evenness in the ground.",
+        "You are completely blind. For a moment you think you hear someone breathing nearby ... "
+        "\n ... surely you must be mistaken.",
+        "Blind, you think you find some sort of object on the ground, but it turns out to be just a stone.",
+        "Blind, you bump into a wall. The wall seems to be covered with some sort of vegetation,"
+        " but its too damp to burn.",
+        "You can't see anything, but the air is damp. It feels like you are far underground.",
+    )
+
+    def at_object_creation(self):
+        super().at_object_creation()
+        self.locks.add("search:all()")
+        self.db.is_lit = False
+        self.db.dark_desc = ""
+
+    def at_init(self):
+        """
+        Called when room is first recached (such as after a reload)
+        """
+        self.check_light_state()
+
+    def return_appearance(self, looker, **kwargs):
+        """
+        This formats a description. It is the hook a 'look' command
+        should call.
+
+        Args:
+            looker (Object): Object doing the looking.
+            **kwargs (dict): Arbitrary, optional arguments for users
+                overriding the call (unused by default).
+        """
+        if not looker:
+            return ""
+        # get and identify all objects
+        visible = (con for con in self.contents if con != looker and con.access(looker, "view"))
+        features, exits, users, things = [], [], [], defaultdict(list)
+        for con in visible:
+            key = con.get_display_name(looker)
+            if con.destination:
+                exits.append(key)
+            elif con.has_account:
+                users.append("|c%s|n" % key)
+            elif con.db.feature_desc:
+                features.append(con.db.feature_desc)
+            else:
+                # things can be pluralized
+                things[key].append(con)
+        # get description, build string
+        string = "{}\n".format(self.get_display_name(looker))
+        string += "-" * 100
+        string += "|/"
+
+        if not self.db.is_lit and not looker.is_superuser:
+            desc = self.db.dark_desc if self.db.dark_desc else random.choice(self.DARK_MESSAGES)
+        else:
+            desc = self.db.desc
+        if desc:
+            string += "{}".format(desc)
+
+        if features and (self.db.is_lit or looker.is_superuser):
+            for feature in features:
+                string += "|/{}".format(feature)
+        if things and (self.db.is_lit or looker.is_superuser):
+            # handle pluralization of things (never pluralize users)
+            thing_strings = []
+            for key, itemlist in sorted(things.items()):
+                nitem = len(itemlist)
+                if nitem == 1:
+                    key, _ = itemlist[0].get_numbered_name(
+                        nitem, looker, key=key)
+                else:
+                    key = [item.get_numbered_name(nitem, looker, key=key)[
+                               1] for item in itemlist][0]
+
+                thing_strings.append(key)
+
+            string += "|/In this place you see "
+
+            if len(thing_strings) == 1:
+                string += "|w{}|n.".format(thing_strings[0])
+            else:
+                for idx, thing in enumerate(thing_strings):
+                    if idx != len(thing_strings) - 1:
+                        string += "|w{}|n, ".format(thing)
+                    else:
+                        string += "and |w{}|n.".format(thing)
+        if exits:
+            string += "|/"
+            string += "-" * 100
+            string += "\n|wExits:|n " + list_to_string(exits)
+        if users:
+            if self.db.is_lit or looker.is_superuser:
+                string += "\n|wYou see:|n " + list_to_string(users)
+            else:
+                string += "\n|wYou sense you are not alone...|n"
+
+        return string
+
+    def check_light_state(self, exclude=None):
+        changed = False
+
+        # there is an object emitting light?
+        if any(self._carries_light(obj) for obj in self.contents if obj != exclude):
+            if not self.db.is_lit:
+                changed = True
+                self.msg_contents(fmt_light("The room lights up."))
+
+            self.db.is_lit = True
+
+            # show objects in room but not chars or exits
+            # for obj in self.contents:
+            #     if not obj.has_account and not obj.destination:
+            #         obj.locks.add('view:all()')
+
+        else:
+            if self.db.is_lit:
+                changed = True
+                self.msg_contents(fmt_dark("Darkness falls."), exclude)
+            # no one is carrying light - darken the room
+            self.db.is_lit = False
+
+            # hidden objects in room but not chars or exits
+            # for obj in self.contents:
+            #     if not obj.has_account and not obj.destination:
+            #         obj.locks.add('view:false()')
+
+        return changed
+
+    def _carries_light(self, obj):
+        """
+        Checks if the given object carries anything that gives light.
+
+        Note that we do NOT look for a specific LightSource typeclass,
+        but for the Attribute is_giving_light - this makes it easy to
+        later add other types of light-giving items. We also accept
+        if there is a light-giving object in the room overall (like if
+        a splinter was dropped in the room)
+        """
+        return (
+                has_effect(obj, 'emit_light') or has_effect(obj, 'emit_magic_light')
+                or any(o for o in obj.contents
+                       if (has_effect(o, 'emit_light') or has_effect(o, 'emit_magic_light'))
+                       and not inherits_from(obj, "typeclasses.objects.ContainerFeature"))
+        )
+
+    def at_object_receive(self, obj, source_location):
+        """
+        Called when an object enters the room.
+        """
+        self.check_light_state()
+
+    def at_object_leave(self, obj, target_location):
+        """
+        In case people leave with the light.
+        This also works if they are teleported away.
+        """
+
+        # do not test for an object going in character's inventory
+        if inherits_from(target_location, Character):
+            # obj.locks.add('view:all()')
+            return
+
+        # since this hook is called while the object is still in the room,
+        # we exclude it from the light check, to ignore any light sources
+        # it may be carrying.
+        self.check_light_state(exclude=obj)
+
+    def get_display_name(self, looker, **kwargs):
+        display_name = super().get_display_name(looker, **kwargs)
+        if self.db.is_lit:
+            display_name = fmt_light(display_name)
+        else:
+            display_name = fmt_dark(display_name)
+
+        return display_name
+
+
+class Zone(DefaultRoom):
+    """
+        Zones are containers for rooms and, for now, is used to
+        provide path-finding capabilities to mob.
+    """
+
+    def at_object_creation(self):
+        super().at_object_creation()
+
+        self.tags.add("zone", category="general")
+        self.locks.add(";".join(["get:false()", "puppet:false()", "view:perm(zone) or perm(Builder)"]))
+
+        self.at_init()
+
+    def at_init(self):
+        super().at_init()
+        # when reloaded recalculate path-finding data
+        self.create_paths()
+
+    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)]
+
+        rooms = search_tag(key=self.name, category="zoneId")
+
+        for room in rooms:
+            self.add_room(room)
+
+    def add_room(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
+        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))
+
+        # avoid inserting room into graph if is already inserted
+        if not self.ndb.sp_graph.is_vertex(room):
+            self.ndb.sp_graph.add_vertex(room)
+            room.tags.add(self.name, category="zoneId")
+            room.db.zone = self
+
+        self.update_room_exits(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)

+ 164 - 0
typeclasses/scripts.py

@@ -0,0 +1,164 @@
+"""
+Scripts
+
+Scripts are powerful jacks-of-all-trades. They have no in-game
+existence and can be used to represent persistent game systems in some
+circumstances. Scripts can also have a time component that allows them
+to "fire" regularly or a limited number of times.
+
+There is generally no "tree" of Scripts inheriting from each other.
+Rather, each script tends to inherit from the base Script class and
+just overloads its hooks to have it perform its function.
+
+"""
+import random
+
+from evennia import DefaultScript, gametime, search_tag, logger
+from evennia.utils import inherits_from
+
+from utils.utils import has_tag, toggle_effect, has_effect
+
+class Script(DefaultScript):
+    """
+    A script type is customized by redefining some or all of its hook
+    methods and variables.
+
+    * available properties
+
+     key (string) - name of object
+     name (string)- same as key
+     aliases (list of strings) - aliases to the object. Will be saved
+              to database as AliasDB entries but returned as strings.
+     dbref (int, read-only) - unique #id-number. Also "id" can be used.
+     date_created (string) - time stamp of object creation
+     permissions (list of strings) - list of permission strings
+
+     desc (string)      - optional description of script, shown in listings
+     obj (Object)       - optional object that this script is connected to
+                          and acts on (set automatically by obj.scripts.add())
+     interval (int)     - how often script should run, in seconds. <0 turns
+                          off ticker
+     start_delay (bool) - if the script should start repeating right away or
+                          wait self.interval seconds
+     repeats (int)      - how many times the script should repeat before
+                          stopping. 0 means infinite repeats
+     persistent (bool)  - if script should survive a server shutdown or not
+     is_active (bool)   - if script is currently running
+
+    * Handlers
+
+     locks - lock-handler: use locks.add() to add new lock strings
+     db - attribute-handler: store/retrieve database attributes on this
+                        self.db.myattr=val, val=self.db.myattr
+     ndb - non-persistent attribute handler: same as db but does not
+                        create a database entry when storing data
+
+    * Helper methods
+
+     start() - start script (this usually happens automatically at creation
+               and obj.script.add() etc)
+     stop()  - stop script, and delete it
+     pause() - put the script on hold, until unpause() is called. If script
+               is persistent, the pause state will survive a shutdown.
+     unpause() - restart a previously paused script. The script will continue
+                 from the paused timer (but at_start() will be called).
+     time_until_next_repeat() - if a timed script (interval>0), returns time
+                 until next tick
+
+    * Hook methods (should also include self as the first argument):
+
+     at_script_creation() - called only once, when an object of this
+                            class is first created.
+     is_valid() - is called to check if the script is valid to be running
+                  at the current time. If is_valid() returns False, the running
+                  script is stopped and removed from the game. You can use this
+                  to check state changes (i.e. an script tracking some combat
+                  stats at regular intervals is only valid to run while there is
+                  actual combat going on).
+      at_start() - Called every time the script is started, which for persistent
+                  scripts is at least once every server start. Note that this is
+                  unaffected by self.delay_start, which only delays the first
+                  call to at_repeat().
+      at_repeat() - Called every self.interval seconds. It will be called
+                  immediately upon launch unless self.delay_start is True, which
+                  will delay the first call of this method by self.interval
+                  seconds. If self.interval==0, this method will never
+                  be called.
+      at_stop() - Called as the script object is stopped and is about to be
+                  removed from the game, e.g. because is_valid() returned False.
+      at_server_reload() - Called when server reloads. Can be used to
+                  save temporary variables you want should survive a reload.
+      at_server_shutdown() - called at a full server shutdown.
+
+    """
+
+    pass
+
+
+class CmdActionScript(Script):
+    """
+    """
+
+    def at_script_creation(self):
+        super().at_script_creation()
+        self.start_delay = True
+        self.persistent = True  # will survive reload
+        self.repeats = 1
+
+    def busy_msg(self):
+        return "doing something else"
+
+class AiManagerScript(Script):
+    """
+    """
+    def at_script_creation(self):
+        super().at_script_creation()
+        self.key = "ai_manager_script"
+        self.desc = "Does things."
+
+    def at_start(self):
+        logger.log_info("[AiManagerScript] starting...")
+        bots = search_tag(key="ai_mob", category="general")
+        logger.log_info("[AiManagerScript] found %d ai aware mobs." % len(bots))
+        logger.log_info("[AiManagerScript] started.")
+
+    def at_stop(self):
+        logger.log_info("[AiManagerScript] stopped.")
+
+    def at_repeat(self):
+        current_time = gametime.gametime()
+        bots = search_tag(key="ai_mob", category="general")
+
+        for bot in bots:
+            bot.think()
+
+            if bot.db.action != None:
+                bot.db.action.update(bot)
+                if bot.db.action.completion_time() <= current_time:
+                    bot.db.action.complete(bot)
+                    bot.db.action.delete()
+
+class Weather(Script):
+    """
+    A timer script that displays weather info. Meant to
+    be attached to a room.
+
+    """
+    def at_script_creation(self):
+        self.key = "weather_script"
+        self.desc = "Gives random weather messages."
+        self.interval = 60 * 1  # every 5 minutes
+        self.persistent = True  # will survive reload
+
+    def at_repeat(self):
+        "called every self.interval seconds."
+        rand = random.random()
+        if rand < 0.5:
+            weather = "A faint breeze is felt."
+        elif rand < 0.7:
+            weather = "Clouds sweep across the sky."
+        else:
+            weather = "There is a light drizzle of rain."
+        # send this message to everyone inside the object this
+        # script is attached to (likely a room)
+        self.obj.msg_contents(weather)

+ 0 - 0
utils/__init__.py


+ 63 - 0
utils/building.py

@@ -0,0 +1,63 @@
+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
+    y = location.db.y
+    if direction == "north":
+        x -= 1
+    if direction == "south":
+        x += 1
+    if direction == "west":
+        y -= 1
+    if direction == "east":
+        y += 1
+
+    destination_id = location.db.zone.ndb.map[x][y]["room_id"]
+    if destination_id == -1:
+        return False
+
+    destinations = search_object(destination_id, exact=True)
+    if not destinations:
+        raise Exception("create_exit: cannot find room {}".format(destination_id))
+    destination = destinations[0]
+
+    # check if exists a room in the selected direction
+    exits = search_object(direction, candidates=location.exits)
+    if exits:
+        exit_obj = exits[0]
+        exit_obj.delete()
+
+    exit_obj, *rest = spawner.spawn(exit_prototype)
+    exit_obj.location = location
+    exit_obj.destination = destination
+    exit_obj.aliases.add(direction)
+
+    if inherits_from(exit_obj, "typeclasses.exits.BaseDoor"):
+        # a door - create its counterpart
+        return_exit, *rest = spawner.spawn(exit_prototype)
+        return_exit.location = destination
+        return_exit.destination = location
+        return_exit.aliases.add(direction)
+
+        exit_obj.db.return_exit = return_exit
+        return_exit.db.return_exit = exit_obj
+
+    return exit_obj

+ 1109 - 0
utils/crafting.py

@@ -0,0 +1,1109 @@
+"""
+Crafting - Griatch 2020
+
+This is a general crafting engine. The basic functionality of crafting is to
+combine any number of of items or tools in a 'recipe' to produce a new result.
+
+    item + item + item + tool + tool  -> recipe -> new result
+
+This is useful not only for traditional crafting but the engine is flexible
+enough to also be useful for puzzles or similar.
+
+## Installation
+
+- Add the `CmdCraft` Command from this module to your default cmdset. This
+  allows for crafting from in-game using a simple syntax.
+- Create a new module and add it to a new list in your settings file
+  (`server/conf/settings.py`) named `CRAFT_RECIPES_MODULES`, such as
+  `CRAFT_RECIPE_MODULES = ["world.recipes_weapons"]`.
+- In the new module(s), create one or more classes, each a child of
+  `CraftingRecipe` from this module. Each such class must have a unique `.name`
+  property. It also defines what inputs are required and what is created using
+  this recipe.
+- Objects to use for crafting should (by default) be tagged with tags using the
+  tag-category `crafting_material` or `crafting_tool`. The name of the object
+  doesn't matter, only its tag.
+
+## Crafting in game
+
+The default `craft` command handles all crafting needs.
+::
+
+    > craft spiked club from club, nails
+
+Here, `spiked club` specifies the recipe while `club` and `nails` are objects
+the crafter must have in their inventory. These will be consumed during
+crafting (by default only if crafting was successful).
+
+A recipe can also require *tools* (like the `hammer` above). These must be
+either in inventory *or* be in the current location. Tools are *not* consumed
+during the crafting process.
+::
+
+    > craft wooden doll from wood with knife
+
+## Crafting in code
+
+In code, you should use the helper function `craft` from this module. This
+specifies the name of the recipe to use and expects all suitable
+ingredients/tools as arguments (consumables and tools should be added together,
+tools will be identified before consumables).
+
+```python
+
+    from evennia.contrib.crafting import crafting
+
+    spiked_club = crafting.craft(crafter, "spiked club", club, nails)
+
+```
+
+The result is always a list with zero or more objects. A fail leads to an empty
+list. The crafter should already have been notified of any error in this case
+(this should be handle by the recipe itself).
+
+## Recipes
+
+A *recipe* is a class that works like an input/output blackbox: you initialize
+it with consumables (and/or tools) if they match the recipe, a new
+result is spit out.  Consumables are consumed in the process while tools are not.
+
+This module contains a base class for making new ingredient types
+(`CraftingRecipeBase`) and an implementation of the most common form of
+crafting (`CraftingRecipe`) using objects and prototypes.
+
+Recipes are put in one or more modules added as a list to the
+`CRAFT_RECIPE_MODULES` setting, for example:
+
+```python
+
+    CRAFT_RECIPE_MODULES = ['world.recipes_weapons', 'world.recipes_potions']
+
+```
+
+Below is an example of a crafting recipe and how `craft` calls it under the
+hood. See the `CraftingRecipe` class for details of which properties and
+methods are available to override - the craft behavior can be modified
+substantially this way.
+
+```python
+
+    from evennia.contrib.crafting.crafting import CraftingRecipe
+
+    class PigIronRecipe(CraftingRecipe):
+        # Pig iron is a high-carbon result of melting iron in a blast furnace.
+
+        name = "pig iron"  # this is what crafting.craft and CmdCraft uses
+        tool_tags = ["blast furnace"]
+        consumable_tags = ["iron ore", "coal", "coal"]
+        output_prototypes = [
+            {"key": "Pig Iron ingot",
+             "desc": "An ingot of crude pig iron.",
+             "tags": [("pig iron", "crafting_material")]}
+        ]
+
+    # for testing, conveniently spawn all we need based on the tags on the class
+    tools, consumables = PigIronRecipe.seed()
+
+    recipe = PigIronRecipe(caller, *(tools + consumables))
+    result = recipe.craft()
+
+```
+
+If the above class was added to a module in `CRAFT_RECIPE_MODULES`, it could be
+called using its `.name` property, as "pig iron".
+
+The [example_recipies](api:evennia.contrib.crafting.example_recipes) module has
+a full example of the components for creating a sword from base components.
+
+----
+
+"""
+
+from copy import copy
+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
+from evennia.prototypes.spawner import spawn
+from evennia.utils.create import create_object, create_script
+
+from typeclasses.scripts import CmdActionScript
+from utils.utils import toggle_effect, indefinite_article, has_effect
+
+_RECIPE_CLASSES = {}
+
+
+def _load_recipes():
+    """
+    Delayed loading of recipe classes. This parses
+    `settings.CRAFT_RECIPE_MODULES`.
+
+    """
+    from django.conf import settings
+
+    global _RECIPE_CLASSES
+    if not _RECIPE_CLASSES:
+        paths = ["evennia.contrib.crafting.example_recipes"]
+        if hasattr(settings, "CRAFT_RECIPE_MODULES"):
+            paths += make_iter(settings.CRAFT_RECIPE_MODULES)
+        for path in paths:
+            for cls in callables_from_module(path).values():
+                if inherits_from(cls, CraftingRecipeBase):
+                    _RECIPE_CLASSES[cls.name] = cls
+
+
+class CraftingError(RuntimeError):
+    """
+    Crafting error.
+
+    """
+
+
+class CraftingValidationError(CraftingError):
+    """
+    Error if crafting validation failed.
+
+    """
+
+
+class CraftingRecipeBase:
+    """
+    The recipe handles all aspects of performing a 'craft' operation. This is
+    the base of the crafting system, intended to be replace if you want to
+    adapt it for very different functionality - see the `CraftingRecipe` child
+    class for an implementation of the most common type of crafting using
+    objects.
+
+    Example of usage:
+    ::
+
+        recipe = CraftRecipe(crafter, obj1, obj2, obj3)
+        result = recipe.craft()
+
+    Note that the most common crafting operation is that the inputs are
+    consumed - so in that case the recipe cannot be used a second time (doing so
+    will raise a `CraftingError`)
+
+    Process:
+
+    1. `.craft(**kwargs)` - this starts the process on the initialized recipe. The kwargs
+       are optional but will be passed into all of the following hooks.
+    2. `.pre_craft(**kwargs)` - this normally validates inputs and stores them in
+       `.validated_inputs.`. Raises `CraftingValidationError` otherwise.
+    4. `.do_craft(**kwargs)` - should return the crafted item(s) or the empty list. Any
+       crafting errors should be immediately reported to user.
+    5. `.post_craft(crafted_result, **kwargs)`- always called, even if `pre_craft`
+       raised a `CraftingError` or `CraftingValidationError`.
+       Should return `crafted_result` (modified or not).
+
+
+    """
+
+    name = "recipe base"
+
+    # if set, allow running `.craft` more than once on the same instance.
+    # don't set this unless crafting inputs are *not* consumed by the crafting
+    # process (otherwise subsequent calls will fail).
+    allow_reuse = False
+
+    def __init__(self, crafter, *inputs, **kwargs):
+        """
+        Initialize the recipe.
+
+        Args:
+            crafter (Object): The one doing the crafting.
+            *inputs (any): The ingredients of the recipe to use.
+            **kwargs (any): Any other parameters that are relevant for
+                this recipe.
+
+        """
+        self.crafter = crafter
+        self.inputs = inputs
+        self.craft_kwargs = kwargs
+        self.allow_craft = True
+        self.validated_inputs = []
+
+    def msg(self, message, **kwargs):
+        """
+        Send message to crafter. This is a central point to override if wanting
+        to change crafting return style in some way.
+
+        Args:
+            message(str): The message to send.
+            **kwargs: Any optional properties relevant to this send.
+
+        """
+        self.crafter.msg(message, {"type": "crafting"})
+
+    def pre_craft(self, **kwargs):
+        """
+        Hook to override.
+
+        This is called just before crafting operation and is normally
+        responsible for validating the inputs, storing data on
+        `self.validated_inputs`.
+
+        Args:
+            **kwargs: Optional extra flags passed during initialization or
+            `.craft(**kwargs)`.
+
+        Raises:
+            CraftingValidationError: If validation fails.
+
+        """
+        if self.allow_craft:
+            self.validated_inputs = self.inputs[:]
+        else:
+            raise CraftingValidationError
+
+    def do_craft(self, **kwargs):
+        """
+        Hook to override.
+
+        This performs the actual crafting. At this point the inputs are
+        expected to have been verified already. If needed, the validated
+        inputs are available on this recipe instance.
+
+        Args:
+            **kwargs: Any extra flags passed at initialization.
+
+        Returns:
+            any: The result of crafting.
+
+        """
+        return None
+
+    def post_craft(self, crafting_result, **kwargs):
+        """
+        Hook to override.
+
+        This is called just after crafting has finished. A common use of this
+        method is to delete the inputs.
+
+        Args:
+            crafting_result (any): The outcome of crafting, as returned by `do_craft`.
+            **kwargs: Any extra flags passed at initialization.
+
+        Returns:
+            any: The final crafting result.
+
+        """
+        return crafting_result
+
+    def craft(self, raise_exception=False, **kwargs):
+        """
+        Main crafting call method. Call this to produce a result and make
+        sure all hooks run correctly.
+
+        Args:
+            raise_exception (bool): If crafting would return `None`, raise
+                exception instead.
+            **kwargs (any): Any other parameters that is relevant
+                for this particular craft operation. This will temporarily
+                override same-named kwargs given at the creation of this recipe
+                and be passed into all of the crafting hooks.
+
+        Returns:
+            any: The result of the craft, or `None` if crafting failed.
+
+        Raises:
+            CraftingValidationError: If recipe validation failed and
+                `raise_exception` is True.
+            CraftingError: On If trying to rerun a no-rerun recipe, or if crafting
+                would return `None` and raise_exception` is set.
+
+        """
+        craft_result = None
+        if self.allow_craft:
+
+            # override/extend craft_kwargs from initialization.
+            craft_kwargs = copy(self.craft_kwargs)
+            craft_kwargs.update(kwargs)
+
+            try:
+                try:
+                    # this assigns to self.validated_inputs
+                    self.pre_craft(**craft_kwargs)
+                except (CraftingError, CraftingValidationError):
+                    if raise_exception:
+                        raise
+                else:
+                    craft_result = self.do_craft(**craft_kwargs)
+                finally:
+                    craft_result = self.post_craft(craft_result, **craft_kwargs)
+            except (CraftingError, CraftingValidationError):
+                if raise_exception:
+                    raise
+
+            # possibly turn off re-use depending on class setting
+            self.allow_craft = self.allow_reuse
+        elif not self.allow_reuse:
+            raise CraftingError("Cannot re-run crafting without re-initializing recipe first.")
+        if craft_result is None and raise_exception:
+            raise CraftingError(f"Crafting of {self.name} failed.")
+        return craft_result
+
+
+class CraftingRecipe(CraftingRecipeBase):
+    """
+    The CraftRecipe implements the most common form of crafting: Combining (and
+    consuming) inputs to produce a new result. This type of recipe only works
+    with typeclassed entities as inputs and outputs, since it's based on Tags
+    and Prototypes.
+
+    There are two types of crafting ingredients: 'tools' and 'consumables'. The
+    difference between them is that the former is not consumed in the crafting
+    process. So if you need a hammer and anvil to craft a sword, they are
+    'tools' whereas the materials of the sword are 'consumables'.
+
+    Examples:
+    ::
+
+        class FlourRecipe(CraftRecipe):
+            name = "flour"
+            tool_tags = ['windmill']
+            consumable_tags = ["wheat"]
+            output_prototypes = [
+                {"key": "Bag of flour",
+                 "typeclass": "typeclasses.food.Flour",
+                 "desc": "A small bag of flour."
+                 "tags": [("flour", "crafting_material"),
+                }
+
+        class BreadRecipe(CraftRecipe):
+            name = "bread"
+            tool_tags = ["roller", "owen"]
+            consumable_tags = ["flour", "egg", "egg", "salt", "water", "yeast"]
+            output_prototypes = [
+                {"key": "bread",
+                 "desc": "A tasty bread."
+                }
+
+
+    ## Properties on the class level:
+
+    - `name` (str): The name of this recipe. This should be globally unique.
+
+    ### tools
+
+    - `tool_tag_category` (str): What tag-category tools must use. Default is
+      'crafting_tool'.
+    - `tool_tags` (list): Object-tags to use for tooling. If more than one instace
+      of a tool is needed, add multiple entries here.
+    - `tool_names` (list): Human-readable names for tools. These are used for informative
+      messages/errors. If not given, the tags will be used. If given, this list should
+      match the length of `tool_tags`.:
+    - `exact_tools` (bool, default True): Must have exactly the right tools, any extra
+      leads to failure.
+    - `exact_tool_order` (bool, default False): Tools must be added in exactly the
+      right order for crafting to pass.
+
+    ### consumables
+
+    - `consumable_tag_category` (str): What tag-category consumables must use.
+      Default is 'crafting_material'.
+    - `consumable_tags` (list): Tags for objects that will be consumed as part of
+      running the recipe.
+    - `consumable_names` (list): Human-readable names for consumables. Same as for tools.
+    - `exact_consumables` (bool, default True): Normally, adding more consumables
+      than needed leads to a a crafting error. If this is False, the craft will
+      still succeed (only the needed ingredients will be consumed).
+    - `exact_consumable_order` (bool, default False): Normally, the order in which
+      ingredients are added does not matter. With this set, trying to add consumables in
+      another order than given will lead to failing crafting.
+    - `consume_on_fail` (bool, default False): Normally, consumables remain if
+      crafting fails. With this flag, a failed crafting will still consume
+      consumables. Note that this will also consume any 'extra' consumables
+      added not part of the recipe!
+
+    ### outputs (result of crafting)
+
+    - `output_prototypes` (list): One or more prototypes (`prototype_keys` or
+      full dicts) describing how to create the result(s) of this recipe.
+    - `output_names` (list): Human-readable names for (prospective) prototypes.
+      This is used in error messages. If not given, this is extracted from the
+      prototypes' `key` if possible.
+
+    ### custom error messages
+
+    custom messages all have custom formatting markers. Many are empty strings
+    when not applicable.
+    ::
+
+        {missing}: Comma-separated list of tool/consumable missing for missing/out of order errors.
+        {excess}: Comma-separated list of tool/consumable added in excess of recipe
+        {inputs}: Comma-separated list of any inputs (tools + consumables) involved in error.
+        {tools}: Comma-sepatated list of tools involved in error.
+        {consumables}: Comma-separated list of consumables involved in error.
+        {outputs}: Comma-separated list of (expected) outputs
+        {t0}..{tN-1}: Individual tools, same order as `.tool_names`.
+        {c0}..{cN-1}: Individual consumables, same order as `.consumable_names`.
+        {o0}..{oN-1}: Individual outputs, same order as `.output_names`.
+
+    - `error_tool_missing_message`: "Could not craft {outputs} without {missing}."
+    - `error_tool_order_message`:
+      "Could not craft {outputs} since {missing} was added in the wrong order."
+    - `error_tool_excess_message`: "Could not craft {outputs} (extra {excess})."
+    - `error_consumable_missing_message`: "Could not craft {outputs} without {missing}."
+    - `error_consumable_order_message`:
+      "Could not craft {outputs} since {missing} was added in the wrong order."
+    - `error_consumable_excess_message`: "Could not craft {outputs} (excess {excess})."
+    - `success_message`: "You successfuly craft {outputs}!"
+    - `failure_message`: ""  (this is handled by the other error messages by default)
+
+    ## Hooks
+
+    1. Crafting starts by calling `.craft(**kwargs)` on the parent class. The
+       `**kwargs` are optional, extends any `**kwargs` passed to the class
+       constructor and will be passed into all the following hooks.
+    3. `.pre_craft(**kwargs)` should handle validation of inputs. Results should
+       be stored in `validated_consumables/tools` respectively. Raises `CraftingValidationError`
+       otherwise.
+    4. `.do_craft(**kwargs)` will not be called if validation failed. Should return
+       a list of the things crafted.
+    5. `.post_craft(crafting_result, **kwargs)` is always called, also if validation
+       failed (`crafting_result` will then be falsy). It does any cleanup. By default
+       this deletes consumables.
+
+    Use `.msg` to conveniently send messages to the crafter. Raise
+    `evennia.contrib.crafting.crafting.CraftingError` exception to abort
+    crafting at any time in the sequence. If raising with a text, this will be
+    shown to the crafter automatically
+
+    """
+
+    name = "crafting recipe"
+
+    # this define the overall category all material tags must have
+    consumable_tag_category = "crafting_material"
+    # tag category for tool objects
+    tool_tag_category = "crafting_tool"
+
+    # the tools needed to perform this crafting. Tools are never consumed (if they were,
+    # they'd need to be a consumable). If more than one instance of a tool is needed,
+    # there should be multiple entries in this list.
+    tool_tags = []
+    # human-readable names for the tools. This will be used for informative messages
+    # or when usage fails. If empty
+    tool_names = []
+    # if we must have exactly the right tools, no more
+    exact_tools = True
+    # if the order of the tools matters
+    exact_tool_order = False
+    # error to show if missing tools
+    error_tool_missing_message = "Could not craft {outputs} without {missing}."
+    # error to show if tool-order matters and it was wrong. Missing is the first
+    # tool out of order
+    error_tool_order_message = (
+        "Could not craft {outputs} since {missing} was added in the wrong order."
+    )
+    # if .exact_tools is set and there are more than needed
+    error_tool_excess_message = (
+        "Could not craft {outputs} without the exact tools (extra {excess})."
+    )
+
+    # a list of tag-keys (of the `tag_category`). If more than one of each type
+    # is needed, there should be multiple same-named entries in this list.
+    consumable_tags = []
+    # these are human-readable names for the items to use. This is used for informative
+    # messages or when usage fails. If empty, the tag-names will be used. If given, this
+    # must have the same length as `consumable_tags`.
+    consumable_names = []
+    # if True, consume valid inputs also if crafting failed (returned None)
+    consume_on_fail = False
+    # if True, having any wrong input result in failing the crafting. If False,
+    # extra components beyond the recipe are ignored.
+    exact_consumables = True
+    # if True, the exact order in which inputs are provided matters and must match
+    # the order of `consumable_tags`. If False, order doesn't matter.
+    exact_consumable_order = False
+    # error to show if missing consumables
+    error_consumable_missing_message = "Could not craft {outputs} without {missing}."
+    # error to show if consumable order matters and it was wrong. Missing is the first
+    # consumable out of order
+    error_consumable_order_message = (
+        "Could not craft {outputs} since {missing} was added in the wrong order."
+    )
+    # if .exact_consumables is set and there are more than needed
+    error_consumable_excess_message = (
+        "Could not craft {outputs} without the exact ingredients (extra {excess})."
+    )
+
+    # this is a list of one or more prototypes (prototype_keys to existing
+    # prototypes or full prototype-dicts) to use to build the result. All of
+    # these will be returned (as a list) if crafting succeeded.
+    output_prototypes = []
+    # human-readable name(s) for the (expected) result of this crafting. This will usually only
+    # be used for error messages (to report what would have been). If not given, the
+    # prototype's key or typeclass will be used. If given, this must have the same length
+    # as `output_prototypes`.
+    output_names = []
+    # general craft-failure msg to show after other error-messages.
+    failure_message = ""
+    # show after a successful craft
+    success_message = "You successfully craft {outputs}!"
+
+    def __init__(self, crafter, *inputs, **kwargs):
+        """
+        Args:
+            crafter (Object): The one doing the crafting.
+            *inputs (Object): The ingredients (+tools) of the recipe to use. The
+                The recipe will itself figure out (from tags) which is a tool and
+                which is a consumable.
+            **kwargs (any): Any other parameters that are relevant for
+                this recipe. These will be passed into the crafting hooks.
+
+        Notes:
+            Internally, this class stores validated data in
+            `.validated_consumables`  and `.validated_tools` respectively. The
+            `.validated_inputs` property (from parent) holds a list of everything
+            types in the order inserted to the class constructor.
+
+        """
+
+        super().__init__(crafter, *inputs, **kwargs)
+
+        self.validated_consumables = []
+        self.validated_tools = []
+
+        # validate class properties
+        if self.consumable_names:
+            assert len(self.consumable_names) == len(self.consumable_tags), (
+                f"Crafting {self.__class__}.consumable_names list must "
+                "have the same length as .consumable_tags."
+            )
+        else:
+            self.consumable_names = self.consumable_tags
+
+        if self.tool_names:
+            assert len(self.tool_names) == len(self.tool_tags), (
+                f"Crafting {self.__class__}.tool_names list must "
+                "have the same length as .tool_tags."
+            )
+        else:
+            self.tool_names = self.tool_tags
+
+        if self.output_names:
+            assert len(self.consumable_names) == len(self.consumable_tags), (
+                f"Crafting {self.__class__}.output_names list must "
+                "have the same length as .output_prototypes."
+            )
+        else:
+            self.output_names = [
+                prot.get("key", prot.get("typeclass", "unnamed"))
+                if isinstance(prot, dict)
+                else str(prot)
+                for prot in self.output_prototypes
+            ]
+
+        assert isinstance(
+            self.output_prototypes, (list, tuple)
+        ), "Crafting {self.__class__}.output_prototypes must be a list or tuple."
+
+        # don't allow reuse if we have consumables. If only tools we can reuse
+        # over and over since nothing changes.
+        self.allow_reuse = not bool(self.consumable_tags)
+
+    def _format_message(self, message, **kwargs):
+
+        missing = iter_to_string(kwargs.get("missing", ""))
+        excess = iter_to_string(kwargs.get("excess", ""))
+        involved_tools = iter_to_string(kwargs.get("tools", ""))
+        involved_cons = iter_to_string(kwargs.get("consumables", ""))
+
+        # build template context
+        mapping = {"missing": missing, "excess": excess}
+        mapping.update(
+            {
+                f"i{ind}": self.consumable_names[ind]
+                for ind, name in enumerate(self.consumable_names or self.consumable_tags)
+            }
+        )
+        mapping.update(
+            {f"o{ind}": self.output_names[ind] for ind, name in enumerate(self.output_names)}
+        )
+        mapping["tools"] = involved_tools
+        mapping["consumables"] = involved_cons
+
+        mapping["inputs"] = iter_to_string(self.consumable_names)
+        mapping["outputs"] = iter_to_string(self.output_names)
+
+        # populate template and return
+        return message.format(**mapping)
+
+    @classmethod
+    def seed(cls, tool_kwargs=None, consumable_kwargs=None):
+        """
+        This is a helper class-method for easy testing and application of this
+        recipe. When called, it will create simple dummy ingredients with names
+        and tags needed by this recipe.
+
+        Args:
+            consumable_kwargs (dict, optional): This will be passed as
+                `**consumable_kwargs` into the `create_object` call for each consumable.
+                If not given, matching `consumable_name` or `consumable_tag`
+                will  be used for key.
+            tool_kwargs (dict, optional): Will be passed as `**tool_kwargs` into the `create_object`
+                call for each tool.  If not given, the matching
+                `tool_name` or `tool_tag` will  be used for key.
+
+        Returns:
+            tuple: A tuple `(tools, consumables)` with newly created dummy
+            objects matching the recipe ingredient list.
+
+        Example:
+        ::
+
+            tools, consumables = SwordRecipe.seed()
+            recipe = SwordRecipe(caller, *(tools + consumables))
+            result = recipe.craft()
+
+        Notes:
+            If `key` is given in `consumable/tool_kwargs` then _every_ created item
+            of each type will have the same key.
+
+        """
+        if not tool_kwargs:
+            tool_kwargs = {}
+        if not consumable_kwargs:
+            consumable_kwargs = {}
+        tool_key = tool_kwargs.pop("key", None)
+        cons_key = consumable_kwargs.pop("key", None)
+        tool_tags = tool_kwargs.pop("tags", [])
+        cons_tags = consumable_kwargs.pop("tags", [])
+
+        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()),
+                    tags=[(tag, cls.tool_tag_category), *tool_tags],
+                    **tool_kwargs,
+                )
+            )
+        consumables = []
+        for itag, tag in enumerate(cls.consumable_tags):
+            consumables.append(
+                create_object(
+                    key=cons_key
+                    or (cls.consumable_names[itag] if cls.consumable_names else tag.capitalize()),
+                    tags=[(tag, cls.consumable_tag_category), *cons_tags],
+                    **consumable_kwargs,
+                )
+            )
+        return tools, consumables
+
+    def pre_craft(self, **kwargs):
+        """
+        Do pre-craft checks, including input validation.
+
+        Check so the given inputs are what is needed. This operates on
+        `self.inputs` which is set to the inputs added to the class
+        constructor. Validated data is stored as lists on `.validated_tools`
+        and `.validated_consumables` respectively.
+
+        Args:
+            **kwargs: Any optional extra kwargs passed during initialization of
+                the recipe class.
+
+        Raises:
+            CraftingValidationError: If validation fails. At this point the crafter
+                is expected to have been informed of the problem already.
+
+        """
+
+        def _check_completeness(
+            tagmap,
+            taglist,
+            namelist,
+            exact_match,
+            exact_order,
+            error_missing_message,
+            error_order_message,
+            error_excess_message,
+        ):
+            """Compare tagmap (inputs) to taglist (required)"""
+            valids = []
+            for itag, tagkey in enumerate(taglist):
+                found_obj = None
+                for obj, objtags in tagmap.items():
+                    if tagkey in objtags:
+                        found_obj = obj
+                        break
+                    if exact_order:
+                        # if we get here order is wrong
+                        err = self._format_message(
+                            error_order_message, missing=obj.get_display_name(looker=self.crafter)
+                        )
+                        self.msg(err)
+                        raise CraftingValidationError(err)
+
+                # since we pop from the mapping, it gets ever shorter
+                match = tagmap.pop(found_obj, None)
+                if match:
+                    valids.append(found_obj)
+                elif exact_match:
+                    err = self._format_message(
+                        error_missing_message,
+                        missing=namelist[itag] if namelist else tagkey.capitalize(),
+                    )
+                    self.msg(err)
+                    raise CraftingValidationError(err)
+
+            if exact_match and tagmap:
+                # something is left in tagmap, that means it was never popped and
+                # thus this is not an exact match
+                err = self._format_message(
+                    error_excess_message,
+                    excess=[obj.get_display_name(looker=self.crafter) for obj in tagmap],
+                )
+                self.msg(err)
+                raise CraftingValidationError(err)
+
+            return valids
+
+        # get tools and consumables from self.inputs
+        tool_map = {
+            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")
+        }
+        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")
+        }
+        consumable_map = {obj: tags for obj, tags in consumable_map.items() if tags}
+
+        # we set these so they are available for error management at all times,
+        # they will be updated with the actual values at the end
+        self.validated_tools = [obj for obj in tool_map]
+        self.validated_consumables = [obj for obj in consumable_map]
+
+        tools = _check_completeness(
+            tool_map,
+            self.tool_tags,
+            self.tool_names,
+            self.exact_tools,
+            self.exact_tool_order,
+            self.error_tool_missing_message,
+            self.error_tool_order_message,
+            self.error_tool_excess_message,
+        )
+        consumables = _check_completeness(
+            consumable_map,
+            self.consumable_tags,
+            self.consumable_names,
+            self.exact_consumables,
+            self.exact_consumable_order,
+            self.error_consumable_missing_message,
+            self.error_consumable_order_message,
+            self.error_consumable_excess_message,
+        )
+
+        # regardless of flags, the tools/consumable lists much contain exactly
+        # all the recipe needs now.
+        if len(tools) != len(self.tool_tags):
+            raise CraftingValidationError(
+                f"Tools {tools}'s tags do not match expected tags {self.tool_tags}"
+            )
+        if len(consumables) != len(self.consumable_tags):
+            raise CraftingValidationError(
+                f"Consumables {consumables}'s tags do not match "
+                f"expected tags {self.consumable_tags}"
+            )
+
+        self.validated_tools = tools
+        self.validated_consumables = consumables
+
+    def do_craft(self, **kwargs):
+        """
+        Hook to override. This will not be called if validation in `pre_craft`
+        fails.
+
+        This performs the actual crafting. At this point the inputs are
+        expected to have been verified already.
+
+        Returns:
+            list: A list of spawned objects created from the inputs, or None
+                on a failure.
+
+        Notes:
+            This method should use `self.msg` to inform the user about the
+            specific reason of failure immediately.
+            We may want to analyze the tools in some way here to affect the
+            crafting process.
+
+        """
+        return spawn(*self.output_prototypes)
+
+    def post_craft(self, craft_result, **kwargs):
+        """
+        Hook to override.
+        This is called just after crafting has finished. A common use of
+        this method is to delete the inputs.
+
+        Args:
+            craft_result (list): The crafted result, provided by `self.do_craft`.
+            **kwargs (any): Passed from `self.craft`.
+
+        Returns:
+            list: The return(s) of the craft, possibly modified in this method.
+
+        Notes:
+            This is _always_ called, also if validation in `pre_craft` fails
+            (`craft_result` will then be `None`).
+
+        """
+        if craft_result:
+            self.msg(self._format_message(self.success_message))
+        elif self.failure_message:
+            self.msg(self._format_message(self.failure_message))
+
+        if craft_result or self.consume_on_fail:
+            # consume the inputs
+            for obj in self.validated_consumables:
+                obj.delete()
+
+        return craft_result
+
+
+# access function
+
+
+def craft(crafter, recipe_name, *inputs, raise_exception=False, **kwargs):
+    """
+    Access function. Craft a given recipe from a source recipe module. A
+    recipe module is a Python module containing recipe classes. Note that this
+    requires `settings.CRAFT_RECIPE_MODULES` to be added to a list of one or
+    more python-paths to modules holding Recipe-classes.
+
+    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: Crafted objects, 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)
+    return recipe.craft(raise_exception=raise_exception)
+
+
+def search_recipe(crafter, recipe_name):
+    # delayed loading/caching of recipes
+    _load_recipes()
+
+    recipe_class = _RECIPE_CLASSES.get(recipe_name, None)
+    if not recipe_class:
+        # try a startswith fuzzy match
+        matches = [key for key in _RECIPE_CLASSES if key.startswith(recipe_name)]
+        if not matches:
+            # try in-match
+            matches = [key for key in _RECIPE_CLASSES if recipe_name in key]
+        if len(matches) == 1:
+            recipe_class = matches[0]
+
+    return recipe_class
+
+
+# craft command/cmdset
+class CraftingCmdSet(CmdSet):
+    """
+    Store crafting command.
+    """
+
+    key = "Crafting cmdset"
+
+    def at_cmdset_creation(self):
+        self.add(CmdCraft())
+
+
+class CmdCraft(Command):
+    """
+    Craft an item using ingredients and tools
+
+    Usage:
+      craft <recipe> [from <ingredient>,...] [using <tool>, ...]
+
+    Examples:
+      craft snowball from snow
+      craft puppet from piece of wood using knife
+      craft bread from flour, butter, water, yeast using owen, bowl, roller
+      craft fireball using wand, spellbook
+
+    Notes:
+        Ingredients must be in the crafter's inventory. Tools can also be
+        things in the current location, like a furnace, windmill or anvil.
+
+    """
+
+    key = "craft"
+    locks = "cmd:all()"
+    help_category = "General"
+    arg_regex = r"\s|$"
+
+    def parse(self):
+        """
+        Handle parsing of:
+        ::
+
+            <recipe> [FROM <ingredients>] [USING <tools>]
+
+        Examples:
+        ::
+
+            craft snowball from snow
+            craft puppet from piece of wood using knife
+            craft bread from flour, butter, water, yeast using owen, bowl, roller
+            craft fireball using wand, spellbook
+
+        """
+        self.args = args = self.args.strip().lower()
+        recipe, ingredients, tools = "", "", ""
+
+        if "from" in args:
+            recipe, *rest = args.split(" from ", 1)
+            rest = rest[0] if rest else ""
+            ingredients, *tools = rest.split(" using ", 1)
+        elif "using" in args:
+            recipe, *tools = args.split(" using ", 1)
+        tools = tools[0] if tools else ""
+
+        self.recipe = recipe.strip()
+        self.ingredients = [ingr.strip() for ingr in ingredients.split(",")]
+        self.tools = [tool.strip() for tool in tools.split(",")]
+
+    def func(self):
+        """
+        Perform crafting.
+
+        Will check the `craft` locktype. If a consumable/ingredient does not pass
+        this check, we will check for the 'crafting_consumable_err_msg'
+        Attribute, otherwise will use a default. If failing on a tool, will use
+        the `crafting_tool_err_msg` if available.
+
+        """
+        caller = self.caller
+
+        if not self.args or not self.recipe:
+            self.caller.msg("Usage: craft <recipe> from <ingredient>, ... [using <tool>,...]")
+            return
+
+        if has_effect(caller, "is_busy"):
+            caller.msg("You are already busy {}.".format(caller.current_action.busy_msg()))
+            return
+
+        ingredients = []
+        for ingr_key in self.ingredients:
+            if not ingr_key:
+                continue
+            obj = caller.search(ingr_key, location=self.caller)
+            # since ingredients are consumed we need extra check so we don't
+            # try to include characters or accounts etc.
+            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)
+            ):
+                # We don't allow to include puppeted objects nor those with the
+                # 'negative' permission 'nocraft'.
+                caller.msg(
+                    obj.attributes.get(
+                        "crafting_consumable_err_msg",
+                        default=f"{obj.get_display_name(looker=caller)} can't be used for this.",
+                    )
+                )
+                return
+            ingredients.append(obj)
+
+        tools = []
+        for tool_key in self.tools:
+            if not tool_key:
+                continue
+            # tools are not consumed, can also exist in the current room
+            obj = caller.search(tool_key)
+            if not obj:
+                return None
+            if not obj.access(caller, "craft", default=True):
+                caller.msg(
+                    obj.attributes.get(
+                        "crafting_tool_err_msg",
+                        default=f"{obj.get_display_name(looker=caller)} can't be used for this.",
+                    )
+                )
+                return
+            tools.append(obj)
+
+        if not search_recipe(caller, self.recipe):
+            caller.msg("You don't know how to craft {} {}.".format(indefinite_article(self.recipe), self.recipe))
+            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.db.current_action = action_script
+
+
+class CmdCraftComplete(CmdActionScript):
+    def at_script_creation(self):
+        super().at_script_creation()
+
+        self.key = "cmd_craft_complete"
+        self.desc = ""
+
+        self.db.recipe = ""
+        self.db.tools_and_ingredients = ""
+
+    def at_repeat(self):
+        caller = self.obj
+
+        if has_effect(caller, "is_busy"):
+            toggle_effect(caller, "is_busy")
+
+        # perform craft and make sure result is in inventory
+        # (the recipe handles all returns to caller)
+        result = craft(caller, self.db.recipe, *self.db.tools_and_ingredients)
+        if result:
+            for obj in result:
+                if inherits_from(obj, "typeclasses.objects.Feature"):
+                    obj.location = caller.location
+                else:
+                    obj.location = caller
+
+    def busy_msg(self):
+        return "crafting {} {}".format(indefinite_article(self.db.recipe), self.db.recipe)
+

+ 71 - 0
utils/priodict.py

@@ -0,0 +1,71 @@
+# Priority dictionary using binary heaps
+# David Eppstein, UC Irvine, 8 Mar 2002
+
+# Implements a data structure that acts almost like a dictionary, with two modifications:
+# (1) D.smallest() returns the value x minimizing D[x].  For this to work correctly,
+#        all values D[x] stored in the dictionary must be comparable.
+# (2) iterating "for x in D" finds and removes the items from D in sorted order.
+#        Each item is not removed until the next item is requested, so D[x] will still
+#        return a useful value until the next iteration of the for-loop.
+# Each operation takes logarithmic amortized time.
+
+from __future__ import generators
+
+class priorityDictionary(dict):
+    def __init__(self):
+        '''Initialize priorityDictionary by creating binary heap of pairs (value,key).
+Note that changing or removing a dict entry will not remove the old pair from the heap
+until it is found by smallest() or until the heap is rebuilt.'''
+        self.__heap = []
+        dict.__init__(self)
+
+    def smallest(self):
+        '''Find smallest item after removing deleted items from front of heap.'''
+        if len(self) == 0:
+            raise IndexError("smallest of empty priorityDictionary")
+        heap = self.__heap
+        while heap[0][1] not in self or self[heap[0][1]] != heap[0][0]:
+            lastItem = heap.pop()
+            insertionPoint = 0
+            while 1:
+                smallChild = 2*insertionPoint+1
+                if smallChild+1 < len(heap) and heap[smallChild] > heap[smallChild+1] :
+                    smallChild += 1
+                if smallChild >= len(heap) or lastItem <= heap[smallChild]:
+                    heap[insertionPoint] = lastItem
+                    break
+                heap[insertionPoint] = heap[smallChild]
+                insertionPoint = smallChild
+        return heap[0][1]
+
+    def __iter__(self):
+        '''Create destructive sorted iterator of priorityDictionary.'''
+        def iterfn():
+            while len(self) > 0:
+                x = self.smallest()
+                yield x
+                del self[x]
+        return iterfn()
+
+    def __setitem__(self,key,val):
+        '''Change value stored in dictionary and add corresponding pair to heap.
+Rebuilds the heap if the number of deleted items gets large, to avoid memory leakage.'''
+        dict.__setitem__(self,key,val)
+        heap = self.__heap
+        if len(heap) > 2 * len(self):
+            self.__heap = [(v,k) for k,v in self.iteritems()]
+            self.__heap.sort()  # builtin sort probably faster than O(n)-time heapify
+        else:
+            newPair = (val,key)
+            insertionPoint = len(heap)
+            heap.append(None)
+            while insertionPoint > 0 and newPair < heap[(insertionPoint-1)//2]:
+                heap[insertionPoint] = heap[(insertionPoint-1)//2]
+                insertionPoint = (insertionPoint-1)//2
+            heap[insertionPoint] = newPair
+
+    def setdefault(self,key,val):
+        '''Reimplement setdefault to pass through our customized __setitem__.'''
+        if key not in self:
+            self[key] = val
+        return self[key]

+ 120 - 0
utils/spath.py

@@ -0,0 +1,120 @@
+# Dijkstra's algorithm for shortest paths
+# David Eppstein, UC Irvine, 4 April 2002
+
+# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/117228
+from utils.priodict import priorityDictionary
+
+def Dijkstra(G,start,end=None):
+    """
+    Find shortest paths from the  start vertex to all vertices nearer than or equal to the end.
+
+    The input graph G is assumed to have the following representation:
+    A vertex can be any object that can be used as an index into a dictionary.
+    G is a dictionary, indexed by vertices.  For any vertex v, G[v] is itself a dictionary,
+    indexed by the neighbors of v.  For any edge v->w, G[v][w] is the length of the edge.
+    This is related to the representation in <http://www.python.org/doc/essays/graphs.html>
+    where Guido van Rossum suggests representing graphs as dictionaries mapping vertices
+    to lists of outgoing edges, however dictionaries of edges have many advantages over lists:
+    they can store extra information (here, the lengths), they support fast existence tests,
+    and they allow easy modification of the graph structure by edge insertion and removal.
+    Such modifications are not needed here but are important in many other graph algorithms.
+    Since dictionaries obey iterator protocol, a graph represented as described here could
+    be handed without modification to an algorithm expecting Guido's graph representation.
+
+    Of course, G and G[v] need not be actual Python dict objects, they can be any other
+    type of object that obeys dict protocol, for instance one could use a wrapper in which vertices
+    are URLs of web pages and a call to G[v] loads the web page and finds its outgoing links.
+
+    The output is a pair (D,P) where D[v] is the distance from start to v and P[v] is the
+    predecessor of v along the shortest path from s to v.
+
+    Dijkstra's algorithm is only guaranteed to work correctly when all edge lengths are positive.
+    This code does not verify this property for all edges (only the edges examined until the end
+    vertex is reached), but will correctly compute shortest paths even for some graphs with negative
+    edges, and will raise an exception if it discovers that a negative edge has caused it to make a mistake.
+    """
+
+    D = {}    # dictionary of final distances
+    P = {}    # dictionary of predecessors
+    Q = priorityDictionary()    # estimated distances of non-final vertices
+    Q[start] = 0
+
+    for v in Q:
+
+        D[v] = Q[v]
+        if v == end: break
+
+        for w in G[v]:
+            vwLength = D[v] + G[v][w]
+            if w in D:
+                if vwLength < D[w]:
+                    raise ValueError("Dijkstra: found better path to already-final vertex")
+            elif w not in Q or vwLength < Q[w]:
+                Q[w] = vwLength
+                P[w] = v
+
+    return (D,P)
+
+def shortestPath(G,start,end):
+    """
+    Find a single shortest path from the given start vertex to the given end vertex.
+    The input has the same conventions as Dijkstra().
+    The output is a list of the vertices in order along the shortest path.
+    """
+
+    D,P = Dijkstra(G,start,end)
+
+    Path = []
+    while 1:
+        Path.append(end)
+        if end == start: break
+        end = P[end]
+    Path.reverse()
+    return Path
+
+class Graph:
+    def __init__(self):
+        self.graph = {}
+
+    def add_vertex(self, vertex):
+        self.graph[vertex] = {}
+
+    def del_vertex(self, vertex):
+        del self.graph[vertex]
+
+    def is_vertex(self, vertex):
+        if vertex in self.graph:
+            return True
+        else:
+            return False
+
+    def add_edge(self, vertex_start, vertex_end, weight, data):
+        self.graph[vertex_start][vertex_end] = weight
+
+    def del_edge(self, vertex_start, vertex_end):
+        del self.graph[vertex_start][vertex_end]
+
+    def is_edge(self, vertex_start, vertex_end):
+        if vertex_start in self.graph and vertex_end in self.graph[vertex_start]:
+            return True
+        else:
+            return False
+
+    def get_graph(self):
+        return self.graph
+
+    def __getitem__(self, key):
+        return self.graph.get(key, {})
+
+    def __len__(self):
+        return len(self.graph)
+
+# example, CLR p.528
+# G = {'s': {'u':10, 'x':5},
+#     'u': {'v':1, 'x':2},
+#     'v': {'y':4},
+#     'x':{'u':3,'v':9,'y':2},
+#     'y':{'s':7,'v':6}}
+#
+# print Dijkstra(G,'s')
+# print shortestPath(G,'s','v')

+ 34 - 0
utils/utils.py

@@ -0,0 +1,34 @@
+from evennia.prototypes import spawner
+
+
+def has_tag(obj, key, category):
+    return obj.tags.get(key=key, category=category) != 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"]
+    value = ""
+    if name[0].lower() in the_vowels:
+        value = "an"
+    else:
+        value = "a"
+
+    return value

+ 0 - 0
web/__init__.py


+ 13 - 0
web/static_overrides/README.md

@@ -0,0 +1,13 @@
+If you want to override one of the static files (such as a CSS or JS file) used by Evennia or a Django app installed in your Evennia project,
+copy it into this directory's corresponding subdirectory, and it will be placed in the static folder when you run:
+
+    python manage.py collectstatic
+
+...or when you reload the server via the command line.
+
+Do note you may have to reproduce any preceeding directory structures for the file to end up in the right place.
+
+Also note that you may need to clear out existing static files for your new ones to be gathered in some cases. Deleting files in static/ 
+will force them to be recollected.
+
+To see what files can be overridden, find where your evennia package is installed, and look in `evennia/web/static/`

+ 3 - 0
web/static_overrides/webclient/css/README.md

@@ -0,0 +1,3 @@
+You can replace the CSS files for Evennia's webclient here.
+
+You can find the original files in `evennia/web/static/webclient/css/`

+ 3 - 0
web/static_overrides/webclient/js/README.md

@@ -0,0 +1,3 @@
+You can replace the javascript files for Evennia's webclient page here.
+
+You can find the original files in `evennia/web/static/webclient/js/`

+ 3 - 0
web/static_overrides/website/css/README.md

@@ -0,0 +1,3 @@
+You can replace the CSS files for Evennia's homepage here.
+
+You can find the original files in `evennia/web/static/website/css/`

+ 3 - 0
web/static_overrides/website/images/README.md

@@ -0,0 +1,3 @@
+You can replace the image files for Evennia's home page here.
+
+You can find the original files in `evennia/web/static/website/images/`

+ 4 - 0
web/template_overrides/README.md

@@ -0,0 +1,4 @@
+Place your own version of templates into this file to override the default ones.
+For instance, if there's a template at: `evennia/web/website/templates/website/index.html`
+and you want to replace it, create the file `template_overrides/website/index.html`
+and it will be loaded instead.

+ 3 - 0
web/template_overrides/webclient/README.md

@@ -0,0 +1,3 @@
+Replace Evennia's webclient django templates with your own here.
+
+You can find the original files in `evennia/web/webclient/templates/webclient/`

+ 7 - 0
web/template_overrides/website/README.md

@@ -0,0 +1,7 @@
+You can replace the django templates (html files) for the website
+here. It uses the default "prosimii" theme. If you want to maintain
+multiple themes rather than just change the default one in-place, 
+make new folders under `template_overrides/` and change
+`settings.ACTIVE_THEME` to point to the folder name to use.
+
+You can find the original files under `evennia/web/website/templates/website/`

+ 3 - 0
web/template_overrides/website/flatpages/README.md

@@ -0,0 +1,3 @@
+Flatpages require a default.html template, which can be overwritten by placing it in this folder.
+
+You can find the original files in `evennia/web/website/templates/website/flatpages/`

+ 3 - 0
web/template_overrides/website/registration/README.md

@@ -0,0 +1,3 @@
+The templates involving login/logout can be overwritten here.
+
+You can find the original files in `evennia/web/website/templates/website/registration/`

+ 18 - 0
web/urls.py

@@ -0,0 +1,18 @@
+"""
+Url definition file to redistribute incoming URL requests to django
+views. Search the Django documentation for "URL dispatcher" for more
+help.
+
+"""
+from django.conf.urls import url, include
+
+# default evennia patterns
+from evennia.web.urls import urlpatterns
+
+# eventual custom patterns
+custom_patterns = [
+    # url(r'/desired/url/', view, name='example'),
+]
+
+# this is required by Django.
+urlpatterns = custom_patterns + urlpatterns

+ 10 - 0
world/README.md

@@ -0,0 +1,10 @@
+# world/
+
+This folder is meant as a miscellanous folder for all that other stuff
+related to the game. Code which are not commands or typeclasses go
+here, like custom economy systems, combat code, batch-files etc. 
+
+You can restructure and even rename this folder as best fits your
+sense of organisation. Just remember that if you add new sub
+directories, you must add (optionally empty) `__init__.py` files in
+them for Python to be able to find the modules within. 

+ 0 - 0
world/__init__.py


+ 26 - 0
world/batch_cmds.ev

@@ -0,0 +1,26 @@
+#
+# A batch-command file is a way to build a game world
+# in a programmatic way, by placing a sequence of
+# build commands after one another. This allows for
+# using a real text editor to edit e.g. descriptions
+# rather than entering text on the command line.
+#
+# A batch-command file is loaded with @batchprocess in-game:
+#
+#   @batchprocess[/interactive] tutorial_examples.batch_cmds
+#
+# A # as the first symbol on a line begins a comment and
+# marks the end of a previous command definition. This is important,
+# - every command must be separated by at least one line of comment.
+#
+# All supplied commands are given as normal, on their own line
+# and accept arguments in any format up until the first next
+# comment line begins. Extra whitespace is removed; an empty
+# line in a command definition translates into a newline.
+#
+# See `evennia/contrib/tutorial_examples/batch_cmds.ev` for
+# an example of a batch-command code. See also the batch-code
+# system for loading python-code this way.
+#
+
+

+ 115 - 0
world/batches/init.ev

@@ -0,0 +1,115 @@
+# We start from limbo. Remember that every command in the batchfile
+# -must- be separated by at least one comment-line.
+@tel #2
+#
+@dig/tel ruined room;start_00:typeclasses.rooms.IndoorRoom
+#
+@desc here =
+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 is overturned against the wall to create a makeshift barricade.
+#
+@create/drop skeleton of a soldier in armor;skeleton;soldier:typeclasses.objects.Feature
+#
+@desc skeleton =
+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.
+#
+@set skeleton/feature_desc = A |wskeleton|n in a broken armor is collapsed on the floor behind the table.
+#
+@lock skeleton = search:all()
+#
+@dig/tel long hall;hall;start_01:typeclasses.rooms.IndoorRoom
+#
+@desc start_01 =
+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.
+#Una lunga sala mattonata da grosse pietre squadrate, le spesse travi di quercia
+#ancora reggono il soffitto affrescato di simboli dorati. Corpuscoli di polvere vorticano
+#nella stanza illuminata, disturbati dal vostro passaggio.
+#
+@open sculpted archway;archway;start_door_00:typeclasses.exits.BaseDoor =  start_00
+#
+@descdoor start_door_00 =
+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.
+#Un'entrata ad arco meravigliosamente scolpita. Due figure sono intagliate nella
+#pietra ai lati della porta, alla destra Its, la musa della manipolazione, alla
+#destra Izzac, la musa dell'autorità.
+#
+@create/drop pile of stones;pile;rubble_01:typeclasses.objects.Feature
+#
+@set rubble_01/feature_desc =
+A |wpile of stones|n and a collapsed beam from the ceiling make it difficult to cross
+this area.
+#Un cumulo di pietre e travi crollate dal soffitto rendono difficoltoso attraversare
+#questa zona.
+#
+@desc rubble_01 =
+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.
+#Un grosso sistema di radici ha perforato il soffitto di questa sala, spezzando una
+#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 =
+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.
+#Una vecchia guardiola devastata dal combattimento avvenuto in queste sale.
+#L'unica parte che è stata risparmiata è il soffitto, completamente ricoperto da
+#affreschi scrostati rappresentati scene di vita marziale.
+#
+@open open doorway;doorway;start_door_01:typeclasses.exits.BaseDoor =  start_01
+#
+@descdoor start_door_01 =
+A large doorway, with no door. The rune '|y◧|n' is engraved on the granite jamb.
+#
+@dig/tel empty corridor;corridor;start_03:typeclasses.rooms.IndoorRoom
+#
+@desc start_03 =
+The sides of the corridor are lined with stone archways, each adorned by a
+stone statue. All the statues have been broken behind recognition.
+#
+@open small doorway;start_door_03:typeclasses.exits.BaseDoor =  start_01
+#
+@descdoor start_door_03 =
+A small doorway, with no door. The rune '|y◓|n' is engraved on the granite jamb.
+#
+@dig/tel ruined temple;temple;start_04:typeclasses.rooms.IndoorRoom
+#
+@desc start_04 =
+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.
+#
+@open large reinforced door;reinforced door;door;start_door_02:typeclasses.exits.BaseDoor =  start_03
+#
+@descdoor start_door_02 =
+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
+#
+zone/addroom tutorial_zone = start_door_01
+#
+zone/addroom tutorial_zone = start_door_02
+#
+zone/addroom tutorial_zone = start_door_03
+#
+zone/addroom tutorial_zone = start_door_04
+#
+@tel start_00
+#

+ 219 - 0
world/prototypes.py

@@ -0,0 +1,219 @@
+"""
+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.
+
+"""
+
+ROOM_EMPTY = {
+    "prototype_key": "room_empty",
+    "key": "empty room",
+    "desc": "An empty room.",
+    "typeclass": "typeclasses.rooms.IndoorRoom"
+}
+
+EXIT_EMPTY = {
+    "prototype_key": "exit_empty",
+    "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'
+}
+
+PLAIN_TROUSERS = {
+    "prototype_key": "plain trousers",
+    "key": "plain trousers",
+    "desc": "Simple but robust cloth trousers.",
+    "typeclass": "typeclasses.objects.EquippableItem",
+    "slot": 'legs'
+}
+
+LEATHER_BOOTS = {
+    "prototype_key": "leather boots",
+    "key": "leather boots",
+    "desc": "A worn pair of leather boots.",
+    "typeclass": "typeclasses.objects.EquippableItem",
+    "slot": 'foot'
+}
+
+FEATURE_CONTAINER = {
+    "prototype_key": "feature_container",
+    "key": "chest",
+    "desc": "A chest.",
+    "feature_desc": "A |wchest|n lies on the floor.",
+    "typeclass": "typeclasses.objects.ContainerFeature"
+}
+
+FEATURE_SKELETON = {
+    "prototype_key": "feature_skeleton",
+    "key": "rugged 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"
+}
+
+STONE = {
+    "prototype_key": "stone",
+    "key": "stone",
+    "desc": "An unremarkable stone made of granite.",
+    "aliases": ["granite stone"],
+    "typeclass": "typeclasses.objects.Item"
+}
+
+BIG_STONE = {
+    "prototype_key": "big stone",
+    "key": "big stone",
+    "desc": "An unremarkable stone made of granite. It seems very heavy.",
+    "aliases": ["big granite stone"],
+    "get_err_msg": "You are not strong enough to lift this stone.",
+    "locks": "get:attr_gt(strength, 50)",
+    "typeclass": "typeclasses.objects.Item"
+}
+
+LANTERN = {
+    "prototype_key": "lantern",
+    "key": "old lantern",
+    "desc": "An old lantern, still filled with oil.",
+    "aliases": ["lantern"],
+    "attrs": [("is_lit", True, None, None)],
+    "tags": [("emit_light", "effect", None)],
+    "locks": "light:all()",
+    "typeclass": "typeclasses.objects.Item"
+}
+
+BLADE_TOOL = {
+    "prototype_key": "blade tool",
+    "key": "steel blade",
+    "desc": "A steel blade, with an oak handle wrapped in cloth.",
+    "aliases": ["blade"],
+    "tags": [("blade", "crafting_tool", None)],
+    "typeclass": "typeclasses.objects.EquippableItem",
+    "slot": 'foot'
+}
+
+WOOD_MATERIAL = {
+    "prototype_key": "wood_material",
+    "key": "piece of wood",
+    "desc": "An unremarkable piece of wood.",
+    "aliases": ["wood"],
+    "tags": [("wood", "crafting_material", None)],
+    "typeclass": "typeclasses.objects.Item"
+}
+
+BLOOD_MATERIAL = {
+    "prototype_key": "blood_material",
+    "key": "vial of blood",
+    "desc": "A vial of blood. Fresh.",
+    "aliases": ["blood, vial"],
+    "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
+## the variable name as `prototype_key` and
+## simple Attributes
+
+# from random import randint
+#
+# GOBLIN = {
+# "key": "goblin grunt",
+# "health": lambda: randint(20,30),
+# "resists": ["cold", "poison"],
+# "attacks": ["fists"],
+# "weaknesses": ["fire", "light"],
+# "tags": = [("greenskin", "monster"), ("humanoid", "monster")]
+# }
+#
+# GOBLIN_WIZARD = {
+# "prototype_parent": "GOBLIN",
+# "key": "goblin wizard",
+# "spells": ["fire ball", "lighting bolt"]
+# }
+#
+# GOBLIN_ARCHER = {
+# "prototype_parent": "GOBLIN",
+# "key": "goblin archer",
+# "attacks": ["short bow"]
+# }
+#
+# This is an example of a prototype without a prototype
+# (nor key) of its own, so it should normally only be
+# used as a mix-in, as in the example of the goblin
+# archwizard below.
+# ARCHWIZARD_MIXIN = {
+# "attacks": ["archwizard staff"],
+# "spells": ["greater fire ball", "greater lighting"]
+# }
+#
+# GOBLIN_ARCHWIZARD = {
+# "key": "goblin archwizard",
+# "prototype_parent" : ("GOBLIN_WIZARD", "ARCHWIZARD_MIXIN")
+# }

+ 23 - 0
world/recipes_base.py

@@ -0,0 +1,23 @@
+from utils.crafting import CraftingRecipe
+
+
+class WoodenPuppetRecipe(CraftingRecipe):
+    """A puppet"""
+    name = "wooden puppet"  # name to refer to this recipe as
+    tool_tags = ["blade"]
+    consumable_tags = ["wood"]
+    output_prototypes = [
+        {"key": "carved wooden doll",
+         "typeclass": "typeclasses.objects.Item",
+         "desc": "A small carved doll"}
+    ]
+
+
+class SummoningCircleRecipe(CraftingRecipe):
+    """A summoning circle"""
+    name = "summoning circle"  # name to refer to this recipe as
+    tool_tags = []
+    consumable_tags = ["blood"]
+    output_prototypes = [
+        "summoning_circle"
+    ]

+ 46 - 0
world/spells.py

@@ -0,0 +1,46 @@
+from evennia import utils, create_script, logger
+from evennia.utils import inherits_from
+
+from typeclasses import effects
+from typeclasses.mobs import Mob
+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])
+
+    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))