From 043cdfc230b1a38e9832a85d664948a2e2dfd57d Mon Sep 17 00:00:00 2001 From: Francesco Cappelli Date: Mon, 10 Jan 2022 14:42:13 +0100 Subject: [PATCH] commit iniziale. --- .gitignore | 52 + README.md | 40 + __init__.py | 6 + commands/README.md | 14 + commands/__init__.py | 0 commands/builder.py | 285 +++++ commands/command.py | 776 ++++++++++++ commands/default_cmdsets.py | 133 ++ server/README.md | 38 + server/__init__.py | 1 + server/conf/__init__.py | 1 + server/conf/at_initial_setup.py | 19 + server/conf/at_search.py | 54 + server/conf/at_server_startstop.py | 63 + server/conf/cmdparser.py | 55 + server/conf/connection_screens.py | 38 + server/conf/inlinefuncs.py | 51 + server/conf/inputfuncs.py | 52 + server/conf/lockfuncs.py | 30 + server/conf/mssp.py | 105 ++ server/conf/portal_services_plugins.py | 24 + server/conf/server_services_plugins.py | 24 + server/conf/serversession.py | 37 + server/conf/settings.py | 50 + server/conf/web_plugins.py | 41 + server/logs/README.md | 15 + typeclasses/README.md | 15 + typeclasses/__init__.py | 0 typeclasses/accounts.py | 104 ++ typeclasses/channels.py | 62 + typeclasses/characters.py | 80 ++ typeclasses/effects.py | 81 ++ typeclasses/exits.py | 171 +++ typeclasses/mob_actions.py | 41 + typeclasses/mobs.py | 47 + typeclasses/objects.py | 264 ++++ typeclasses/rooms.py | 287 +++++ typeclasses/scripts.py | 164 +++ utils/__init__.py | 0 utils/building.py | 63 + utils/crafting.py | 1109 +++++++++++++++++ utils/priodict.py | 71 ++ utils/spath.py | 120 ++ utils/utils.py | 34 + web/__init__.py | 0 web/static_overrides/README.md | 13 + web/static_overrides/webclient/css/README.md | 3 + web/static_overrides/webclient/js/README.md | 3 + web/static_overrides/website/css/README.md | 3 + web/static_overrides/website/images/README.md | 3 + web/template_overrides/README.md | 4 + web/template_overrides/webclient/README.md | 3 + web/template_overrides/website/README.md | 7 + .../website/flatpages/README.md | 3 + .../website/registration/README.md | 3 + web/urls.py | 18 + world/README.md | 10 + world/__init__.py | 0 world/batch_cmds.ev | 26 + world/batches/init.ev | 115 ++ world/prototypes.py | 219 ++++ world/recipes_base.py | 23 + world/spells.py | 46 + 63 files changed, 5219 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 __init__.py create mode 100644 commands/README.md create mode 100644 commands/__init__.py create mode 100644 commands/builder.py create mode 100644 commands/command.py create mode 100644 commands/default_cmdsets.py create mode 100644 server/README.md create mode 100644 server/__init__.py create mode 100644 server/conf/__init__.py create mode 100644 server/conf/at_initial_setup.py create mode 100644 server/conf/at_search.py create mode 100644 server/conf/at_server_startstop.py create mode 100644 server/conf/cmdparser.py create mode 100644 server/conf/connection_screens.py create mode 100644 server/conf/inlinefuncs.py create mode 100644 server/conf/inputfuncs.py create mode 100644 server/conf/lockfuncs.py create mode 100644 server/conf/mssp.py create mode 100644 server/conf/portal_services_plugins.py create mode 100644 server/conf/server_services_plugins.py create mode 100644 server/conf/serversession.py create mode 100644 server/conf/settings.py create mode 100644 server/conf/web_plugins.py create mode 100644 server/logs/README.md create mode 100644 typeclasses/README.md create mode 100644 typeclasses/__init__.py create mode 100644 typeclasses/accounts.py create mode 100644 typeclasses/channels.py create mode 100644 typeclasses/characters.py create mode 100644 typeclasses/effects.py create mode 100644 typeclasses/exits.py create mode 100644 typeclasses/mob_actions.py create mode 100644 typeclasses/mobs.py create mode 100644 typeclasses/objects.py create mode 100644 typeclasses/rooms.py create mode 100644 typeclasses/scripts.py create mode 100644 utils/__init__.py create mode 100644 utils/building.py create mode 100644 utils/crafting.py create mode 100644 utils/priodict.py create mode 100644 utils/spath.py create mode 100644 utils/utils.py create mode 100644 web/__init__.py create mode 100644 web/static_overrides/README.md create mode 100644 web/static_overrides/webclient/css/README.md create mode 100644 web/static_overrides/webclient/js/README.md create mode 100644 web/static_overrides/website/css/README.md create mode 100644 web/static_overrides/website/images/README.md create mode 100644 web/template_overrides/README.md create mode 100644 web/template_overrides/webclient/README.md create mode 100644 web/template_overrides/website/README.md create mode 100644 web/template_overrides/website/flatpages/README.md create mode 100644 web/template_overrides/website/registration/README.md create mode 100644 web/urls.py create mode 100644 world/README.md create mode 100644 world/__init__.py create mode 100644 world/batch_cmds.ev create mode 100644 world/batches/init.ev create mode 100644 world/prototypes.py create mode 100644 world/recipes_base.py create mode 100644 world/spells.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5fb664 --- /dev/null +++ b/.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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..fe62bd3 --- /dev/null +++ b/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! diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..6e3dbee --- /dev/null +++ b/__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. + +""" diff --git a/commands/README.md b/commands/README.md new file mode 100644 index 0000000..0425ce6 --- /dev/null +++ b/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. diff --git a/commands/__init__.py b/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/commands/builder.py b/commands/builder.py new file mode 100644 index 0000000..a1e8215 --- /dev/null +++ b/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 = + + 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 = ") + 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 diff --git a/commands/command.py b/commands/command.py new file mode 100644 index 0000000..a334a94 --- /dev/null +++ b/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 [from ] + + 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: + [FROM ] + """ + 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 + + 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 in + """ + + key = "put" + locks = "cmd:all()" + help_category = "General" + + def parse(self): + """ + Handle parsing of: + in + """ + 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 in ") + 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 + close + + """ + + key = "op" + aliases = ["close"] + locks = "cmd:all()" + help_category = "General" + + def func(self): + if not self.args: + self.caller.msg("Usage: open||close ") + 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: + :: + [at ] + """ + 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 [at ]") + 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 diff --git a/commands/default_cmdsets.py b/commands/default_cmdsets.py new file mode 100644 index 0000000..8849f45 --- /dev/null +++ b/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. + # diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..9f530c3 --- /dev/null +++ b/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. diff --git a/server/__init__.py b/server/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/server/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/server/conf/__init__.py b/server/conf/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/server/conf/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/server/conf/at_initial_setup.py b/server/conf/at_initial_setup.py new file mode 100644 index 0000000..b394a04 --- /dev/null +++ b/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 diff --git a/server/conf/at_search.py b/server/conf/at_search.py new file mode 100644 index 0000000..f7f8b6c --- /dev/null +++ b/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. + + """ diff --git a/server/conf/at_server_startstop.py b/server/conf/at_server_startstop.py new file mode 100644 index 0000000..98c29fa --- /dev/null +++ b/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 diff --git a/server/conf/cmdparser.py b/server/conf/cmdparser.py new file mode 100644 index 0000000..831990a --- /dev/null +++ b/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 diff --git a/server/conf/connection_screens.py b/server/conf/connection_screens.py new file mode 100644 index 0000000..5f46b4c --- /dev/null +++ b/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 |n + If you need to create an account, type (without the <>'s): + |wcreate |n + + Enter |whelp|n for more info. |wlook|n will re-show this screen. +|b==============================================================|n""".format( + settings.SERVERNAME, utils.get_evennia_version("short") +) diff --git a/server/conf/inlinefuncs.py b/server/conf/inlinefuncs.py new file mode 100644 index 0000000..1190597 --- /dev/null +++ b/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() diff --git a/server/conf/inputfuncs.py b/server/conf/inputfuncs.py new file mode 100644 index 0000000..6cb226f --- /dev/null +++ b/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 diff --git a/server/conf/lockfuncs.py b/server/conf/lockfuncs.py new file mode 100644 index 0000000..8dac0c7 --- /dev/null +++ b/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 diff --git a/server/conf/mssp.py b/server/conf/mssp.py new file mode 100644 index 0000000..270c8f5 --- /dev/null +++ b/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", +} diff --git a/server/conf/portal_services_plugins.py b/server/conf/portal_services_plugins.py new file mode 100644 index 0000000..b536c56 --- /dev/null +++ b/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 diff --git a/server/conf/server_services_plugins.py b/server/conf/server_services_plugins.py new file mode 100644 index 0000000..e3d41fe --- /dev/null +++ b/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 diff --git a/server/conf/serversession.py b/server/conf/serversession.py new file mode 100644 index 0000000..13fbf1e --- /dev/null +++ b/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 diff --git a/server/conf/settings.py b/server/conf/settings.py new file mode 100644 index 0000000..21b2923 --- /dev/null +++ b/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.") diff --git a/server/conf/web_plugins.py b/server/conf/web_plugins.py new file mode 100644 index 0000000..ec11ad7 --- /dev/null +++ b/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 diff --git a/server/logs/README.md b/server/logs/README.md new file mode 100644 index 0000000..35ad999 --- /dev/null +++ b/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_.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. diff --git a/typeclasses/README.md b/typeclasses/README.md new file mode 100644 index 0000000..7a77739 --- /dev/null +++ b/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. diff --git a/typeclasses/__init__.py b/typeclasses/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/typeclasses/accounts.py b/typeclasses/accounts.py new file mode 100644 index 0000000..ba293c6 --- /dev/null +++ b/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 diff --git a/typeclasses/channels.py b/typeclasses/channels.py new file mode 100644 index 0000000..0b943d0 --- /dev/null +++ b/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 diff --git a/typeclasses/characters.py b/typeclasses/characters.py new file mode 100644 index 0000000..5169021 --- /dev/null +++ b/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 + diff --git a/typeclasses/effects.py b/typeclasses/effects.py new file mode 100644 index 0000000..a44544d --- /dev/null +++ b/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() diff --git a/typeclasses/exits.py b/typeclasses/exits.py new file mode 100644 index 0000000..b698bd3 --- /dev/null +++ b/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 diff --git a/typeclasses/mob_actions.py b/typeclasses/mob_actions.py new file mode 100644 index 0000000..5781596 --- /dev/null +++ b/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 diff --git a/typeclasses/mobs.py b/typeclasses/mobs.py new file mode 100644 index 0000000..be3f64a --- /dev/null +++ b/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) diff --git a/typeclasses/objects.py b/typeclasses/objects.py new file mode 100644 index 0000000..e19f89c --- /dev/null +++ b/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 , 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 diff --git a/typeclasses/rooms.py b/typeclasses/rooms.py new file mode 100644 index 0000000..56e5691 --- /dev/null +++ b/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) diff --git a/typeclasses/scripts.py b/typeclasses/scripts.py new file mode 100644 index 0000000..47188d1 --- /dev/null +++ b/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) diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/building.py b/utils/building.py new file mode 100644 index 0000000..ab7b179 --- /dev/null +++ b/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 diff --git a/utils/crafting.py b/utils/crafting.py new file mode 100644 index 0000000..d3daaa1 --- /dev/null +++ b/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 [from ,...] [using , ...] + + 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: + :: + + [FROM ] [USING ] + + 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 from , ... [using ,...]") + 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) + diff --git a/utils/priodict.py b/utils/priodict.py new file mode 100644 index 0000000..edb41c4 --- /dev/null +++ b/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] diff --git a/utils/spath.py b/utils/spath.py new file mode 100644 index 0000000..c7c7c7a --- /dev/null +++ b/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 + 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') diff --git a/utils/utils.py b/utils/utils.py new file mode 100644 index 0000000..65c8aed --- /dev/null +++ b/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 \ No newline at end of file diff --git a/web/__init__.py b/web/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/static_overrides/README.md b/web/static_overrides/README.md new file mode 100644 index 0000000..ab9a09e --- /dev/null +++ b/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/` diff --git a/web/static_overrides/webclient/css/README.md b/web/static_overrides/webclient/css/README.md new file mode 100644 index 0000000..6ab7cbb --- /dev/null +++ b/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/` diff --git a/web/static_overrides/webclient/js/README.md b/web/static_overrides/webclient/js/README.md new file mode 100644 index 0000000..c785cb1 --- /dev/null +++ b/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/` diff --git a/web/static_overrides/website/css/README.md b/web/static_overrides/website/css/README.md new file mode 100644 index 0000000..004fcd8 --- /dev/null +++ b/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/` diff --git a/web/static_overrides/website/images/README.md b/web/static_overrides/website/images/README.md new file mode 100644 index 0000000..2d2060c --- /dev/null +++ b/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/` diff --git a/web/template_overrides/README.md b/web/template_overrides/README.md new file mode 100644 index 0000000..87ba6f1 --- /dev/null +++ b/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. diff --git a/web/template_overrides/webclient/README.md b/web/template_overrides/webclient/README.md new file mode 100644 index 0000000..b69d627 --- /dev/null +++ b/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/` diff --git a/web/template_overrides/website/README.md b/web/template_overrides/website/README.md new file mode 100644 index 0000000..589823a --- /dev/null +++ b/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/` diff --git a/web/template_overrides/website/flatpages/README.md b/web/template_overrides/website/flatpages/README.md new file mode 100644 index 0000000..9cd8142 --- /dev/null +++ b/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/` diff --git a/web/template_overrides/website/registration/README.md b/web/template_overrides/website/registration/README.md new file mode 100644 index 0000000..7c0dfbe --- /dev/null +++ b/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/` diff --git a/web/urls.py b/web/urls.py new file mode 100644 index 0000000..741706c --- /dev/null +++ b/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 diff --git a/world/README.md b/world/README.md new file mode 100644 index 0000000..0f3862d --- /dev/null +++ b/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. diff --git a/world/__init__.py b/world/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/world/batch_cmds.ev b/world/batch_cmds.ev new file mode 100644 index 0000000..ff5469e --- /dev/null +++ b/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. +# + + diff --git a/world/batches/init.ev b/world/batches/init.ev new file mode 100644 index 0000000..af62602 --- /dev/null +++ b/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 +# diff --git a/world/prototypes.py b/world/prototypes.py new file mode 100644 index 0000000..1e26c15 --- /dev/null +++ b/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") +# } diff --git a/world/recipes_base.py b/world/recipes_base.py new file mode 100644 index 0000000..70f56e7 --- /dev/null +++ b/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" + ] diff --git a/world/spells.py b/world/spells.py new file mode 100644 index 0000000..ca5e22c --- /dev/null +++ b/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)) \ No newline at end of file