From 6f56ba71ce06f69ff947d08a2c4bbd4640f033ea Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 23 Sep 2017 13:55:01 +0200 Subject: [PATCH 01/68] Make internal/external ports clearer in wake of port changes --- evennia/server/portal/portal.py | 26 ++++++++++++-------------- evennia/server/server.py | 4 ++-- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index c5c9740ef9..fa4008a58a 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -192,7 +192,7 @@ if AMP_ENABLED: from evennia.server import amp - print(' amp (to Server): %s' % AMP_PORT) + print(' amp (to Server): %s (internal)' % AMP_PORT) factory = amp.AmpClientFactory(PORTAL) amp_client = internet.TCPClient(AMP_HOST, AMP_PORT, factory) @@ -223,7 +223,7 @@ if TELNET_ENABLED: telnet_service.setName('EvenniaTelnet%s' % pstring) PORTAL.services.addService(telnet_service) - print(' telnet%s: %s' % (ifacestr, port)) + print(' telnet%s: %s (external)' % (ifacestr, port)) if SSL_ENABLED: @@ -249,7 +249,7 @@ if SSL_ENABLED: ssl_service.setName('EvenniaSSL%s' % pstring) PORTAL.services.addService(ssl_service) - print(" ssl%s: %s" % (ifacestr, port)) + print(" ssl%s: %s (external)" % (ifacestr, port)) if SSH_ENABLED: @@ -273,7 +273,7 @@ if SSH_ENABLED: ssh_service.setName('EvenniaSSH%s' % pstring) PORTAL.services.addService(ssh_service) - print(" ssh%s: %s" % (ifacestr, port)) + print(" ssh%s: %s (external)" % (ifacestr, port)) if WEBSERVER_ENABLED: @@ -287,7 +287,6 @@ if WEBSERVER_ENABLED: if interface not in ('0.0.0.0', '::') or len(WEBSERVER_INTERFACES) > 1: ifacestr = "-%s" % interface for proxyport, serverport in WEBSERVER_PORTS: - pstring = "%s:%s<->%s" % (ifacestr, proxyport, serverport) web_root = EvenniaReverseProxyResource('127.0.0.1', serverport, '') webclientstr = "" if WEBCLIENT_ENABLED: @@ -305,21 +304,20 @@ if WEBSERVER_ENABLED: from evennia.server.portal import webclient from evennia.utils.txws import WebSocketFactory - interface = WEBSOCKET_CLIENT_INTERFACE + w_interface = WEBSOCKET_CLIENT_INTERFACE + w_ifacestr = '' + if w_interface not in ('0.0.0.0', '::') or len(WEBSERVER_INTERFACES) > 1: + w_ifacestr = "-%s" % interface port = WEBSOCKET_CLIENT_PORT - ifacestr = "" - if interface not in ('0.0.0.0', '::'): - ifacestr = "-%s" % interface - pstring = "%s:%s" % (ifacestr, port) factory = protocol.ServerFactory() factory.noisy = False factory.protocol = webclient.WebSocketClient factory.sessionhandler = PORTAL_SESSIONS - websocket_service = internet.TCPServer(port, WebSocketFactory(factory), interface=interface) - websocket_service.setName('EvenniaWebSocket%s' % pstring) + websocket_service = internet.TCPServer(port, WebSocketFactory(factory), interface=w_interface) + websocket_service.setName('EvenniaWebSocket%s:%s' % (w_ifacestr, proxyport)) PORTAL.services.addService(websocket_service) websocket_started = True - webclientstr = "\n + webclient%s" % pstring + webclientstr = "\n + webclient-websocket%s: %s (external)" % (w_ifacestr, proxyport) web_root = Website(web_root, logPath=settings.HTTP_LOG_FILE) proxy_service = internet.TCPServer(proxyport, @@ -327,7 +325,7 @@ if WEBSERVER_ENABLED: interface=interface) proxy_service.setName('EvenniaWebProxy%s' % pstring) PORTAL.services.addService(proxy_service) - print(" webproxy%s:%s (<-> %s)%s" % (ifacestr, proxyport, serverport, webclientstr)) + print(" website-proxy%s: %s (external) %s" % (ifacestr, proxyport, webclientstr)) for plugin_module in PORTAL_SERVICES_PLUGIN_MODULES: diff --git a/evennia/server/server.py b/evennia/server/server.py index 27ca1fd0f8..fb9f753b5c 100644 --- a/evennia/server/server.py +++ b/evennia/server/server.py @@ -546,7 +546,7 @@ if AMP_ENABLED: ifacestr = "" if AMP_INTERFACE != '127.0.0.1': ifacestr = "-%s" % AMP_INTERFACE - print(' amp (to Portal)%s: %s' % (ifacestr, AMP_PORT)) + print(' amp (to Portal)%s: %s (internal)' % (ifacestr, AMP_PORT)) from evennia.server import amp @@ -586,7 +586,7 @@ if WEBSERVER_ENABLED: webserver.setName('EvenniaWebServer%s' % serverport) EVENNIA.services.addService(webserver) - print(" webserver: %s" % serverport) + print(" webserver: %s (internal)" % serverport) ENABLED = [] if IRC_ENABLED: From f75860b1039de4b505dcf6cc78cb50883876bef6 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 23 Sep 2017 14:26:37 +0200 Subject: [PATCH 02/68] Update github contribute instructions for new branch layout --- .github/CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 353140b4d7..a4d41c6bbf 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -19,6 +19,10 @@ happens to be named "command"! A PR allows you to request that your custom fixes/additions/changes be pulled into the main Evennia repository. To make a PR you must first [fork Evennia on GitHub][8]. Read the [Contribution][3] page for more help. +If you are working to solve an Issue in the issue tracker, note which branch you should make the PR +against (`master` or `develop`). If you are making a PR for a new feature or contrib, do so against +the `develop' branch. + - All contributions should abide by Evennia's [style guide](https://github.com/evennia/evennia/blob/master/CODING_STYLE.md). - For your own sanity and ours, separate unrelated contributions into their own branches and make a new PR for each. You can still update the branch after the PR is up - the PR will update automatically. From 18e55847050f97bee2c4649168d461705323c440 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 1 Oct 2017 09:43:33 +0200 Subject: [PATCH 03/68] Fix unittest for mail contrib update --- evennia/contrib/mail.py | 10 +++++----- evennia/contrib/tests.py | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/evennia/contrib/mail.py b/evennia/contrib/mail.py index 48369083d1..6e8585136d 100644 --- a/evennia/contrib/mail.py +++ b/evennia/contrib/mail.py @@ -133,7 +133,7 @@ class CmdMail(default_cmds.MuxCommand): return else: all_mail = self.get_all_mail() - mind_max = all_mail.count() - 1 + mind_max = max(0, all_mail.count() - 1) mind = max(0, min(mind_max, int(self.lhs) - 1)) if all_mail[mind]: all_mail[mind].delete() @@ -154,7 +154,7 @@ class CmdMail(default_cmds.MuxCommand): return else: all_mail = self.get_all_mail() - mind_max = all_mail.count() - 1 + mind_max = max(0, all_mail.count() - 1) if "/" in self.rhs: message_number, message = self.rhs.split("/", 1) mind = max(0, min(mind_max, int(message_number) - 1)) @@ -193,7 +193,7 @@ class CmdMail(default_cmds.MuxCommand): return else: all_mail = self.get_all_mail() - mind_max = all_mail.count() - 1 + mind_max = max(0, all_mail.count() - 1) mind = max(0, min(mind_max, int(self.lhs) - 1)) if all_mail[mind]: old_message = all_mail[mind] @@ -218,9 +218,9 @@ class CmdMail(default_cmds.MuxCommand): self.send_mail(self.search_targets(self.lhslist), subject, body, self.caller) else: all_mail = self.get_all_mail() - mind_max = all_mail.count() - 1 + mind_max = max(0, all_mail.count() - 1) try: - mind = max(0, min(mind_max, self.lhs - 1)) + mind = max(0, min(mind_max, int(self.lhs) - 1)) message = all_mail[mind] except (ValueError, IndexError): self.caller.msg("'%s' is not a valid mail id." % self.lhs) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 2342c4d17a..1678d06567 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -661,6 +661,7 @@ from evennia.contrib import mail class TestMail(CommandTest): def test_mail(self): self.call(mail.CmdMail(), "2", "'2' is not a valid mail id.", caller=self.account) + self.call(mail.CmdMail(), "test", "'test' is not a valid mail id.") self.call(mail.CmdMail(), "", "There are no messages in your inbox.", caller=self.account) self.call(mail.CmdMail(), "Char=Message 1", "You have received a new @mail from Char|You sent your message.", caller=self.char1) self.call(mail.CmdMail(), "Char=Message 2", "You sent your message.", caller=self.char2) From c3ce2ebcd7c134df9c02c202d90159777693d2cc Mon Sep 17 00:00:00 2001 From: Scyfris Talivinsky Date: Sat, 30 Sep 2017 18:18:54 -0700 Subject: [PATCH 04/68] Fix locationless spawned objects Spawned objects were not getting locations assigned to them. By default the locations should be assigned to the caller's location. --- evennia/commands/default/building.py | 2 +- evennia/utils/spawner.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 61a46bde30..9170f3c9ff 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2718,7 +2718,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): self.caller.msg("The prototype must be a prototype key or a Python dictionary.") return - if "noloc" in self.switches and not "location" not in prototype: + if "noloc" not in self.switches and "location" not in prototype: prototype["location"] = self.caller.location for obj in spawn(prototype): diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 423fc8225b..4a8ac946c8 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -100,7 +100,7 @@ _CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") def _handle_dbref(inp): - dbid_to_obj(inp, ObjectDB) + return dbid_to_obj(inp, ObjectDB) def _validate_prototype(key, prototype, protparents, visited): From dd8e136cfc7db7e6d24c5c84562152ec34fd539a Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Tue, 26 Sep 2017 15:01:52 -0700 Subject: [PATCH 05/68] Added at_before hooks to CmdGet, CmdGive, CmdDrop Added in references to at_before hooks for the get, give, and drop commands. If these hooks return 'False' or 'None', the action is canceled. --- evennia/commands/default/general.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index c5075b8744..481a1df993 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -266,6 +266,10 @@ class CmdGet(COMMAND_DEFAULT_CLASS): else: caller.msg("You can't get that.") return + + # calling at_before_get hook method + if not obj.at_before_get(caller): + return obj.move_to(caller, quiet=True) caller.msg("You pick up %s." % obj.name) @@ -273,7 +277,7 @@ class CmdGet(COMMAND_DEFAULT_CLASS): (caller.name, obj.name), exclude=caller) - # calling hook method + # calling at_get hook method obj.at_get(caller) @@ -307,6 +311,10 @@ class CmdDrop(COMMAND_DEFAULT_CLASS): multimatch_string="You carry more than one %s:" % self.args) if not obj: return + + # Call the object script's at_before_drop() method. + if not obj.at_before_drop(caller): + return obj.move_to(caller.location, quiet=True) caller.msg("You drop %s." % (obj.name,)) @@ -350,6 +358,11 @@ class CmdGive(COMMAND_DEFAULT_CLASS): if not to_give.location == caller: caller.msg("You are not holding %s." % to_give.key) return + + # calling at_before_give hook method + if not to_give.at_before_give(caller, target): + return + # give object caller.msg("You give %s to %s." % (to_give.key, target.key)) to_give.move_to(target, quiet=True) From 5ab670f3c8c0cac0dfd81d468ce6416b365e36ed Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Tue, 26 Sep 2017 15:04:04 -0700 Subject: [PATCH 06/68] Added at_before_give, at_before_get, and at_before_drop Added in new hooks to the default object - at_before_give, at_before_get, and at_before_drop. By default, these hooks do nothing but return True - however, if overloaded, they can be used to execute code before an object is given, gotten, or dropped, and even prevent the action if made to return None or False. --- evennia/objects/objects.py | 66 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 4479da8af7..29658cabab 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1492,6 +1492,25 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): """ pass + def at_before_get(self, getter, **kwargs): + """ + Called by the default `get` command before this object has been + picked up. + + Args: + getter (Object): The object about to get this object. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + + Returns: + shouldget (bool): If the object should be gotten or not. + + Notes: + If this method returns False/None, the getting is cancelled + before it is even started. + """ + return True + def at_get(self, getter, **kwargs): """ Called by the default `get` command when this object has been @@ -1504,11 +1523,32 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): Notes: This hook cannot stop the pickup from happening. Use - permissions for that. + permissions or the at_before_get() hook for that. """ pass + def at_before_give(self, giver, getter, **kwargs): + """ + Called by the default `give` command before this object has been + given. + + Args: + giver (Object): The object about to give this object. + getter (Object): The object about to get this object. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + + Returns: + shouldgive (bool): If the object should be given or not. + + Notes: + If this method returns False/None, the giving is cancelled + before it is even started. + + """ + return True + def at_give(self, giver, getter, **kwargs): """ Called by the default `give` command when this object has been @@ -1522,11 +1562,31 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): Notes: This hook cannot stop the give from happening. Use - permissions for that. + permissions or the at_before_give() hook for that. """ pass + def at_before_drop(self, dropper, **kwargs): + """ + Called by the default `drop` command before this object has been + dropped. + + Args: + dropper (Object): The object which will drop this object. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + + Returns: + shoulddrop (bool): If the object should be dropped or not. + + Notes: + If this method returns False/None, the dropping is cancelled + before it is even started. + + """ + return True + def at_drop(self, dropper, **kwargs): """ Called by the default `drop` command when this object has been @@ -1539,7 +1599,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): Notes: This hook cannot stop the drop from happening. Use - permissions from that. + permissions or the at_before_drop() hook for that. """ pass From d2e8badf18c92c064d0d66d671b54109000d3b9a Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 1 Oct 2017 16:51:41 +0200 Subject: [PATCH 07/68] Stop #dbref searches for unprivileged. Other minor fixes. Resolves #1251. --- evennia/commands/default/account.py | 2 +- evennia/commands/default/general.py | 10 +++++----- evennia/objects/objects.py | 25 +++++++++++++++---------- evennia/typeclasses/models.py | 4 ++++ 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/evennia/commands/default/account.py b/evennia/commands/default/account.py index 01b7066fe3..e45e2a9347 100644 --- a/evennia/commands/default/account.py +++ b/evennia/commands/default/account.py @@ -824,7 +824,7 @@ class CmdQuell(COMMAND_DEFAULT_CLASS): """Perform the command""" account = self.account permstr = account.is_superuser and " (superuser)" or "(%s)" % (", ".join(account.permissions.all())) - if self.cmdstring == '@unquell': + if self.cmdstring in ('unquell', '@unquell'): if not account.attributes.get('_quell'): self.msg("Already using normal Account permissions %s." % permstr) else: diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 481a1df993..2f880f3f7f 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -67,7 +67,7 @@ class CmdLook(COMMAND_DEFAULT_CLASS): caller.msg("You have no location to look at!") return else: - target = caller.search(self.args, use_dbref=caller.check_permstring("Builders")) + target = caller.search(self.args) if not target: return self.msg(caller.at_look(target)) @@ -266,7 +266,7 @@ class CmdGet(COMMAND_DEFAULT_CLASS): else: caller.msg("You can't get that.") return - + # calling at_before_get hook method if not obj.at_before_get(caller): return @@ -311,7 +311,7 @@ class CmdDrop(COMMAND_DEFAULT_CLASS): multimatch_string="You carry more than one %s:" % self.args) if not obj: return - + # Call the object script's at_before_drop() method. if not obj.at_before_drop(caller): return @@ -358,11 +358,11 @@ class CmdGive(COMMAND_DEFAULT_CLASS): if not to_give.location == caller: caller.msg("You are not holding %s." % to_give.key) return - + # calling at_before_give hook method if not to_give.at_before_give(caller, target): return - + # give object caller.msg("You give %s to %s." % (to_give.key, target.key)) to_give.move_to(target, quiet=True) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 29658cabab..e0fd0dace4 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -292,7 +292,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): candidates=None, nofound_string=None, multimatch_string=None, - use_dbref=True): + use_dbref=None): """ Returns an Object matching a search string/condition @@ -343,8 +343,9 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): caller's contents (inventory). nofound_string (str): optional custom string for not-found error message. multimatch_string (str): optional custom string for multimatch error header. - use_dbref (bool, optional): if False, treat a given #dbref strings as a - normal string rather than database ids. + use_dbref (bool or None, optional): if True/False, active/deactivate the use of + #dbref as valid global search arguments. If None, check against a permission + ('Builder' by default). Returns: match (Object, None or list): will return an Object/None if `quiet=False`, @@ -360,6 +361,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): """ is_string = isinstance(searchdata, basestring) + if is_string: # searchdata is a string; wrap some common self-references if searchdata.lower() in ("here", ): @@ -367,6 +369,9 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): if searchdata.lower() in ("me", "self",): return [self] if quiet else self + if use_dbref is None: + use_dbref = self.locks.check_lockstring(self, "_dummy:perm(Builder)") + if use_nicks: # do nick-replacement on search searchdata = self.nicks.nickreplace(searchdata, categories=("object", "account"), include_account=True) @@ -1495,7 +1500,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): def at_before_get(self, getter, **kwargs): """ Called by the default `get` command before this object has been - picked up. + picked up. Args: getter (Object): The object about to get this object. @@ -1509,8 +1514,8 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): If this method returns False/None, the getting is cancelled before it is even started. """ - return True - + return True + def at_get(self, getter, **kwargs): """ Called by the default `get` command when this object has been @@ -1545,10 +1550,10 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): Notes: If this method returns False/None, the giving is cancelled before it is even started. - + """ - return True - + return True + def at_give(self, giver, getter, **kwargs): """ Called by the default `give` command when this object has been @@ -1586,7 +1591,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): """ return True - + def at_drop(self, dropper, **kwargs): """ Called by the default `drop` command when this object has been diff --git a/evennia/typeclasses/models.py b/evennia/typeclasses/models.py index 8a20ba9148..c156aa2ac6 100644 --- a/evennia/typeclasses/models.py +++ b/evennia/typeclasses/models.py @@ -575,6 +575,10 @@ class TypedObject(SharedMemoryModel): ppos = _PERMISSION_HIERARCHY.index(perm) return any(True for hpos, hperm in enumerate(_PERMISSION_HIERARCHY) if hperm in perms and hpos > ppos) + # we ignore pluralization (english only) + if perm.endswith("s"): + return self.check_permstring(perm[:-1]) + return False # From 9620ddb2e08975a5f55bc8f5bf25e43a67532b65 Mon Sep 17 00:00:00 2001 From: Edwin Sutanto Date: Sun, 8 Oct 2017 10:02:29 +0700 Subject: [PATCH 08/68] Fix ImportError for SSH client Added instruction to also install pyasn1. --- evennia/server/portal/ssh.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/server/portal/ssh.py b/evennia/server/portal/ssh.py index e993e74bc3..7dde511637 100644 --- a/evennia/server/portal/ssh.py +++ b/evennia/server/portal/ssh.py @@ -20,9 +20,9 @@ from twisted.conch.interfaces import IConchUser _SSH_IMPORT_ERROR = """ ERROR: Missing crypto library for SSH. Install it with - pip install cryptography + pip install cryptography pyasn1 -(On older Twisted versions you may have to do 'pip install pycrypto pyasn1 instead). +(On older Twisted versions you may have to do 'pip install pycrypto pyasn1' instead). If you get a compilation error you must install a C compiler and the SSL dev headers (On Debian-derived systems this is the gcc and libssl-dev From 5cb1ce5b6e60d967b94f0f87e479de73e1b4da1e Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 8 Oct 2017 20:04:00 -0700 Subject: [PATCH 09/68] Move turnbattle.py to turnbattle/tb_basic.py Moves the basic turnbattle module to a new module file in a new 'turnbattle' subfolder. Also fixes a minor bug where the first character in the turn order was not being initialized properly at the start of a fight. --- evennia/contrib/turnbattle/tb_basic.py | 738 +++++++++++++++++++++++++ 1 file changed, 738 insertions(+) create mode 100644 evennia/contrib/turnbattle/tb_basic.py diff --git a/evennia/contrib/turnbattle/tb_basic.py b/evennia/contrib/turnbattle/tb_basic.py new file mode 100644 index 0000000000..f1ff33a483 --- /dev/null +++ b/evennia/contrib/turnbattle/tb_basic.py @@ -0,0 +1,738 @@ +""" +Simple turn-based combat system + +Contrib - Tim Ashley Jenkins 2017 + +This is a framework for a simple turn-based combat system, similar +to those used in D&D-style tabletop role playing games. It allows +any character to start a fight in a room, at which point initiative +is rolled and a turn order is established. Each participant in combat +has a limited time to decide their action for that turn (30 seconds by +default), and combat progresses through the turn order, looping through +the participants until the fight ends. + +Only simple rolls for attacking are implemented here, but this system +is easily extensible and can be used as the foundation for implementing +the rules from your turn-based tabletop game of choice or making your +own battle system. + +To install and test, import this module's BattleCharacter object into +your game's character.py module: + + from evennia.contrib.turnbattle.tb_basic import BattleCharacter + +And change your game's character typeclass to inherit from BattleCharacter +instead of the default: + + class Character(BattleCharacter): + +Next, import this module into your default_cmdsets.py module: + + from evennia.contrib.turnbattle import tb_basic + +And add the battle command set to your default command set: + + # + # any commands you add below will overload the default ones. + # + self.add(tb_basic.BattleCmdSet()) + +This module is meant to be heavily expanded on, so you may want to copy it +to your game's 'world' folder and modify it there rather than importing it +in your game and using it as-is. +""" + +from random import randint +from evennia import DefaultCharacter, Command, default_cmds, DefaultScript +from evennia.commands.default.help import CmdHelp + +""" +---------------------------------------------------------------------------- +COMBAT FUNCTIONS START HERE +---------------------------------------------------------------------------- +""" + + +def roll_init(character): + """ + Rolls a number between 1-1000 to determine initiative. + + Args: + character (obj): The character to determine initiative for + + Returns: + initiative (int): The character's place in initiative - higher + numbers go first. + + Notes: + By default, does not reference the character and simply returns + a random integer from 1 to 1000. + + Since the character is passed to this function, you can easily reference + a character's stats to determine an initiative roll - for example, if your + character has a 'dexterity' attribute, you can use it to give that character + an advantage in turn order, like so: + + return (randint(1,20)) + character.db.dexterity + + This way, characters with a higher dexterity will go first more often. + """ + return randint(1, 1000) + + +def get_attack(attacker, defender): + """ + Returns a value for an attack roll. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being attacked + + Returns: + attack_value (int): Attack roll value, compared against a defense value + to determine whether an attack hits or misses. + + Notes: + By default, returns a random integer from 1 to 100 without using any + properties from either the attacker or defender. + + This can easily be expanded to return a value based on characters stats, + equipment, and abilities. This is why the attacker and defender are passed + to this function, even though nothing from either one are used in this example. + """ + # For this example, just return a random integer up to 100. + attack_value = randint(1, 100) + return attack_value + + +def get_defense(attacker, defender): + """ + Returns a value for defense, which an attack roll must equal or exceed in order + for an attack to hit. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being attacked + + Returns: + defense_value (int): Defense value, compared against an attack roll + to determine whether an attack hits or misses. + + Notes: + By default, returns 50, not taking any properties of the defender or + attacker into account. + + As above, this can be expanded upon based on character stats and equipment. + """ + # For this example, just return 50, for about a 50/50 chance of hit. + defense_value = 50 + return defense_value + + +def get_damage(attacker, defender): + """ + Returns a value for damage to be deducted from the defender's HP after abilities + successful hit. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being damaged + + Returns: + damage_value (int): Damage value, which is to be deducted from the defending + character's HP. + + Notes: + By default, returns a random integer from 15 to 25 without using any + properties from either the attacker or defender. + + Again, this can be expanded upon. + """ + # For this example, just generate a number between 15 and 25. + damage_value = randint(15, 25) + return damage_value + + +def apply_damage(defender, damage): + """ + Applies damage to a target, reducing their HP by the damage amount to a + minimum of 0. + + Args: + defender (obj): Character taking damage + damage (int): Amount of damage being taken + """ + defender.db.hp -= damage # Reduce defender's HP by the damage dealt. + # If this reduces it to 0 or less, set HP to 0. + if defender.db.hp <= 0: + defender.db.hp = 0 + + +def resolve_attack(attacker, defender, attack_value=None, defense_value=None): + """ + Resolves an attack and outputs the result. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being attacked + + Notes: + Even though the attack and defense values are calculated + extremely simply, they are separated out into their own functions + so that they are easier to expand upon. + """ + # Get an attack roll from the attacker. + if not attack_value: + attack_value = get_attack(attacker, defender) + # Get a defense value from the defender. + if not defense_value: + defense_value = get_defense(attacker, defender) + # If the attack value is lower than the defense value, miss. Otherwise, hit. + if attack_value < defense_value: + attacker.location.msg_contents("%s's attack misses %s!" % (attacker, defender)) + else: + damage_value = get_damage(attacker, defender) # Calculate damage value. + # Announce damage dealt and apply damage. + attacker.location.msg_contents("%s hits %s for %i damage!" % (attacker, defender, damage_value)) + apply_damage(defender, damage_value) + # If defender HP is reduced to 0 or less, announce defeat. + if defender.db.hp <= 0: + attacker.location.msg_contents("%s has been defeated!" % defender) + + +def combat_cleanup(character): + """ + Cleans up all the temporary combat-related attributes on a character. + + Args: + character (obj): Character to have their combat attributes removed + + Notes: + Any attribute whose key begins with 'combat_' is temporary and no + longer needed once a fight ends. + """ + for attr in character.attributes.all(): + if attr.key[:7] == "combat_": # If the attribute name starts with 'combat_'... + character.attributes.remove(key=attr.key) # ...then delete it! + + +def is_in_combat(character): + """ + Returns true if the given character is in combat. + + Args: + character (obj): Character to determine if is in combat or not + + Returns: + (bool): True if in combat or False if not in combat + """ + if character.db.Combat_TurnHandler: + return True + return False + + +def is_turn(character): + """ + Returns true if it's currently the given character's turn in combat. + + Args: + character (obj): Character to determine if it is their turn or not + + Returns: + (bool): True if it is their turn or False otherwise + """ + turnhandler = character.db.Combat_TurnHandler + currentchar = turnhandler.db.fighters[turnhandler.db.turn] + if character == currentchar: + return True + return False + + +def spend_action(character, actions, action_name=None): + """ + Spends a character's available combat actions and checks for end of turn. + + Args: + character (obj): Character spending the action + actions (int) or 'all': Number of actions to spend, or 'all' to spend all actions + + Kwargs: + action_name (str or None): If a string is given, sets character's last action in + combat to provided string + """ + if action_name: + character.db.Combat_LastAction = action_name + if actions == 'all': # If spending all actions + character.db.Combat_ActionsLeft = 0 # Set actions to 0 + else: + character.db.Combat_ActionsLeft -= actions # Use up actions. + if character.db.Combat_ActionsLeft < 0: + character.db.Combat_ActionsLeft = 0 # Can't have fewer than 0 actions + character.db.Combat_TurnHandler.turn_end_check(character) # Signal potential end of turn. + + +""" +---------------------------------------------------------------------------- +CHARACTER TYPECLASS +---------------------------------------------------------------------------- +""" + + +class BattleCharacter(DefaultCharacter): + """ + A character able to participate in turn-based combat. Has attributes for current + and maximum HP, and access to combat commands. + """ + + def at_object_creation(self): + """ + Called once, when this object is first created. This is the + normal hook to overload for most object types. + """ + self.db.max_hp = 100 # Set maximum HP to 100 + self.db.hp = self.db.max_hp # Set current HP to maximum + """ + Adds attributes for a character's current and maximum HP. + We're just going to set this value at '100' by default. + + You may want to expand this to include various 'stats' that + can be changed at creation and factor into combat calculations. + """ + + def at_before_move(self, destination): + """ + Called just before starting to move this object to + destination. + + Args: + destination (Object): The object we are moving to + + Returns: + shouldmove (bool): If we should move or not. + + Notes: + If this method returns False/None, the move is cancelled + before it is even started. + + """ + # Keep the character from moving if at 0 HP or in combat. + if is_in_combat(self): + self.msg("You can't exit a room while in combat!") + return False # Returning false keeps the character from moving. + if self.db.HP <= 0: + self.msg("You can't move, you've been defeated!") + return False + return True + + +""" +---------------------------------------------------------------------------- +COMMANDS START HERE +---------------------------------------------------------------------------- +""" + + +class CmdFight(Command): + """ + Starts a fight with everyone in the same room as you. + + Usage: + fight + + When you start a fight, everyone in the room who is able to + fight is added to combat, and a turn order is randomly rolled. + When it's your turn, you can attack other characters. + """ + key = "fight" + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + here = self.caller.location + fighters = [] + + if not self.caller.db.hp: # If you don't have any hp + self.caller.msg("You can't start a fight if you've been defeated!") + return + if is_in_combat(self.caller): # Already in a fight + self.caller.msg("You're already in a fight!") + return + for thing in here.contents: # Test everything in the room to add it to the fight. + if thing.db.HP: # If the object has HP... + fighters.append(thing) # ...then add it to the fight. + if len(fighters) <= 1: # If you're the only able fighter in the room + self.caller.msg("There's nobody here to fight!") + return + if here.db.Combat_TurnHandler: # If there's already a fight going on... + here.msg_contents("%s joins the fight!" % self.caller) + here.db.Combat_TurnHandler.join_fight(self.caller) # Join the fight! + return + here.msg_contents("%s starts a fight!" % self.caller) + # Add a turn handler script to the room, which starts combat. + here.scripts.add("contrib.turnbattle.tb_basic.TurnHandler") + # Remember you'll have to change the path to the script if you copy this code to your own modules! + + +class CmdAttack(Command): + """ + Attacks another character. + + Usage: + attack + + When in a fight, you may attack another character. The attack has + a chance to hit, and if successful, will deal damage. + """ + + key = "attack" + help_category = "combat" + + def func(self): + "This performs the actual command." + "Set the attacker to the caller and the defender to the target." + + if not is_in_combat(self.caller): # If not in combat, can't attack. + self.caller.msg("You can only do that in combat. (see: help fight)") + return + + if not is_turn(self.caller): # If it's not your turn, can't attack. + self.caller.msg("You can only do that on your turn.") + return + + if not self.caller.db.hp: # Can't attack if you have no HP. + self.caller.msg("You can't attack, you've been defeated.") + return + + attacker = self.caller + defender = self.caller.search(self.args) + + if not defender: # No valid target given. + return + + if not defender.db.hp: # Target object has no HP left or to begin with + self.caller.msg("You can't fight that!") + return + + if attacker == defender: # Target and attacker are the same + self.caller.msg("You can't attack yourself!") + return + + "If everything checks out, call the attack resolving function." + resolve_attack(attacker, defender) + spend_action(self.caller, 1, action_name="attack") # Use up one action. + + +class CmdPass(Command): + """ + Passes on your turn. + + Usage: + pass + + When in a fight, you can use this command to end your turn early, even + if there are still any actions you can take. + """ + + key = "pass" + aliases = ["wait", "hold"] + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + if not is_in_combat(self.caller): # Can only pass a turn in combat. + self.caller.msg("You can only do that in combat. (see: help fight)") + return + + if not is_turn(self.caller): # Can only pass if it's your turn. + self.caller.msg("You can only do that on your turn.") + return + + self.caller.location.msg_contents("%s takes no further action, passing the turn." % self.caller) + spend_action(self.caller, 'all', action_name="pass") # Spend all remaining actions. + + +class CmdDisengage(Command): + """ + Passes your turn and attempts to end combat. + + Usage: + disengage + + Ends your turn early and signals that you're trying to end + the fight. If all participants in a fight disengage, the + fight ends. + """ + + key = "disengage" + aliases = ["spare"] + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + if not is_in_combat(self.caller): # If you're not in combat + self.caller.msg("You can only do that in combat. (see: help fight)") + return + + if not is_turn(self.caller): # If it's not your turn + self.caller.msg("You can only do that on your turn.") + return + + self.caller.location.msg_contents("%s disengages, ready to stop fighting." % self.caller) + spend_action(self.caller, 'all', action_name="disengage") # Spend all remaining actions. + """ + The action_name kwarg sets the character's last action to "disengage", which is checked by + the turn handler script to see if all fighters have disengaged. + """ + + +class CmdRest(Command): + """ + Recovers damage. + + Usage: + rest + + Resting recovers your HP to its maximum, but you can only + rest if you're not in a fight. + """ + + key = "rest" + help_category = "combat" + + def func(self): + "This performs the actual command." + + if is_in_combat(self.caller): # If you're in combat + self.caller.msg("You can't rest while you're in combat.") + return + + self.caller.db.hp = self.caller.db.max_hp # Set current HP to maximum + self.caller.location.msg_contents("%s rests to recover HP." % self.caller) + """ + You'll probably want to replace this with your own system for recovering HP. + """ + + +class CmdCombatHelp(CmdHelp): + """ + View help or a list of topics + + Usage: + help + help list + help all + + This will search for help on commands and other + topics related to the game. + """ + # Just like the default help command, but will give quick + # tips on combat when used in a fight with no arguments. + + def func(self): + if is_in_combat(self.caller) and not self.args: # In combat and entered 'help' alone + self.caller.msg("Available combat commands:|/" + + "|wAttack:|n Attack a target, attempting to deal damage.|/" + + "|wPass:|n Pass your turn without further action.|/" + + "|wDisengage:|n End your turn and attempt to end combat.|/") + else: + super(CmdCombatHelp, self).func() # Call the default help command + + +class BattleCmdSet(default_cmds.CharacterCmdSet): + """ + This command set includes all the commmands used in the battle system. + """ + key = "DefaultCharacter" + + def at_cmdset_creation(self): + """ + Populates the cmdset + """ + self.add(CmdFight()) + self.add(CmdAttack()) + self.add(CmdRest()) + self.add(CmdPass()) + self.add(CmdDisengage()) + self.add(CmdCombatHelp()) + + +""" +---------------------------------------------------------------------------- +SCRIPTS START HERE +---------------------------------------------------------------------------- +""" + + +class TurnHandler(DefaultScript): + """ + This is the script that handles the progression of combat through turns. + On creation (when a fight is started) it adds all combat-ready characters + to its roster and then sorts them into a turn order. There can only be one + fight going on in a single room at a time, so the script is assigned to a + room as its object. + + Fights persist until only one participant is left with any HP or all + remaining participants choose to end the combat with the 'disengage' command. + """ + + def at_script_creation(self): + """ + Called once, when the script is created. + """ + self.key = "Combat Turn Handler" + self.interval = 5 # Once every 5 seconds + self.persistent = True + self.db.fighters = [] + + # Add all fighters in the room with at least 1 HP to the combat." + for object in self.obj.contents: + if object.db.hp: + self.db.fighters.append(object) + + # Initialize each fighter for combat + for fighter in self.db.fighters: + self.initialize_for_combat(fighter) + + # Add a reference to this script to the room + self.obj.db.Combat_TurnHandler = self + + # Roll initiative and sort the list of fighters depending on who rolls highest to determine turn order. + # The initiative roll is determined by the roll_init function and can be customized easily. + ordered_by_roll = sorted(self.db.fighters, key=roll_init, reverse=True) + self.db.fighters = ordered_by_roll + + # Announce the turn order. + self.obj.msg_contents("Turn order is: %s " % ", ".join(obj.key for obj in self.db.fighters)) + + # Start first fighter's turn. + self.start_turn(self.db.fighters[0]) + + # Set up the current turn and turn timeout delay. + self.db.turn = 0 + self.db.timer = 30 # 30 seconds + + def at_stop(self): + """ + Called at script termination. + """ + for fighter in self.db.fighters: + combat_cleanup(fighter) # Clean up the combat attributes for every fighter. + self.obj.db.Combat_TurnHandler = None # Remove reference to turn handler in location + + def at_repeat(self): + """ + Called once every self.interval seconds. + """ + currentchar = self.db.fighters[self.db.turn] # Note the current character in the turn order. + self.db.timer -= self.interval # Count down the timer. + + if self.db.timer <= 0: + # Force current character to disengage if timer runs out. + self.obj.msg_contents("%s's turn timed out!" % currentchar) + spend_action(currentchar, 'all', action_name="disengage") # Spend all remaining actions. + return + elif self.db.timer <= 10 and not self.db.timeout_warning_given: # 10 seconds left + # Warn the current character if they're about to time out. + currentchar.msg("WARNING: About to time out!") + self.db.timeout_warning_given = True + + def initialize_for_combat(self, character): + """ + Prepares a character for combat when starting or entering a fight. + + Args: + character (obj): Character to initialize for combat. + """ + combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case. + character.db.Combat_ActionsLeft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0 + character.db.Combat_TurnHandler = self # Add a reference to this turn handler script to the character + character.db.Combat_LastAction = "null" # Track last action taken in combat + + def start_turn(self, character): + """ + Readies a character for the start of their turn by replenishing their + available actions and notifying them that their turn has come up. + + Args: + character (obj): Character to be readied. + + Notes: + Here, you only get one action per turn, but you might want to allow more than + one per turn, or even grant a number of actions based on a character's + attributes. You can even add multiple different kinds of actions, I.E. actions + separated for movement, by adding "character.db.Combat_MovesLeft = 3" or + something similar. + """ + character.db.Combat_ActionsLeft = 1 # 1 action per turn. + # Prompt the character for their turn and give some information. + character.msg("|wIt's your turn! You have %i HP remaining.|n" % character.db.hp) + + def next_turn(self): + """ + Advances to the next character in the turn order. + """ + + # Check to see if every character disengaged as their last action. If so, end combat. + disengage_check = True + for fighter in self.db.fighters: + if fighter.db.Combat_LastAction != "disengage": # If a character has done anything but disengage + disengage_check = False + if disengage_check: # All characters have disengaged + self.obj.msg_contents("All fighters have disengaged! Combat is over!") + self.stop() # Stop this script and end combat. + return + + # Check to see if only one character is left standing. If so, end combat. + defeated_characters = 0 + for fighter in self.db.fighters: + if fighter.db.HP == 0: + defeated_characters += 1 # Add 1 for every fighter with 0 HP left (defeated) + if defeated_characters == (len(self.db.fighters) - 1): # If only one character isn't defeated + for fighter in self.db.fighters: + if fighter.db.HP != 0: + LastStanding = fighter # Pick the one fighter left with HP remaining + self.obj.msg_contents("Only %s remains! Combat is over!" % LastStanding) + self.stop() # Stop this script and end combat. + return + + # Cycle to the next turn. + currentchar = self.db.fighters[self.db.turn] + self.db.turn += 1 # Go to the next in the turn order. + if self.db.turn > len(self.db.fighters) - 1: + self.db.turn = 0 # Go back to the first in the turn order once you reach the end. + newchar = self.db.fighters[self.db.turn] # Note the new character + self.db.timer = 30 + self.time_until_next_repeat() # Reset the timer. + self.db.timeout_warning_given = False # Reset the timeout warning. + self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar)) + self.start_turn(newchar) # Start the new character's turn. + + def turn_end_check(self, character): + """ + Tests to see if a character's turn is over, and cycles to the next turn if it is. + + Args: + character (obj): Character to test for end of turn + """ + if not character.db.Combat_ActionsLeft: # Character has no actions remaining + self.next_turn() + return + + def join_fight(self, character): + """ + Adds a new character to a fight already in progress. + + Args: + character (obj): Character to be added to the fight. + """ + # Inserts the fighter to the turn order, right behind whoever's turn it currently is. + self.db.fighters.insert(self.db.turn, character) + # Tick the turn counter forward one to compensate. + self.db.turn += 1 + # Initialize the character like you do at the start. + self.initialize_for_combat(character) From 83791e619a52f1fc2e6d8f90d6b2c4fa401d87dc Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 8 Oct 2017 20:05:53 -0700 Subject: [PATCH 10/68] Add 'tb_equip', weapon & armor system for 'turnbattle' Adds a new module, 'tb_equip', an implementation of the 'turnbattle' system that includes weapons and armor, which can be wielded and donned to modify attack damage and accuracy. --- evennia/contrib/turnbattle/tb_equip.py | 1067 ++++++++++++++++++++++++ 1 file changed, 1067 insertions(+) create mode 100644 evennia/contrib/turnbattle/tb_equip.py diff --git a/evennia/contrib/turnbattle/tb_equip.py b/evennia/contrib/turnbattle/tb_equip.py new file mode 100644 index 0000000000..702d02520e --- /dev/null +++ b/evennia/contrib/turnbattle/tb_equip.py @@ -0,0 +1,1067 @@ +""" +Simple turn-based combat system with equipment + +Contrib - Tim Ashley Jenkins 2017 + +This is a version of the 'turnbattle' contrib with a basic system for +weapons and armor implemented. Weapons can have unique damage ranges +and accuracy modifiers, while armor can reduce incoming damage and +change one's chance of getting hit. The 'wield' command is used to +equip weapons and the 'don' command is used to equip armor. + +Some prototypes are included at the end of this module - feel free to +copy them into your game's prototypes.py module in your 'world' folder +and create them with the @spawn command. (See the tutorial for using +the @spawn command for details.) + +For the example equipment given, heavier weapons deal more damage +but are less accurate, while light weapons are more accurate but +deal less damage. Similarly, heavy armor reduces incoming damage by +a lot but increases your chance of getting hit, while light armor is +easier to dodge in but reduces incoming damage less. Light weapons are +more effective against lightly armored opponents and heavy weapons are +more damaging against heavily armored foes, but heavy weapons and armor +are slightly better than light weapons and armor overall. + +This is a fairly bare implementation of equipment that is meant to be +expanded to fit your game - weapon and armor slots, damage types and +damage bonuses, etc. should be fairly simple to implement according to +the rules of your preferred system or the needs of your own game. + +To install and test, import this module's BattleCharacter object into +your game's character.py module: + + from evennia.contrib.turnbattle.tb_equip import BattleCharacter + +And change your game's character typeclass to inherit from BattleCharacter +instead of the default: + + class Character(BattleCharacter): + +Next, import this module into your default_cmdsets.py module: + + from evennia.contrib.turnbattle import tb_equip + +And add the battle command set to your default command set: + + # + # any commands you add below will overload the default ones. + # + self.add(tb_equip.BattleCmdSet()) + +This module is meant to be heavily expanded on, so you may want to copy it +to your game's 'world' folder and modify it there rather than importing it +in your game and using it as-is. +""" + +from random import randint +from evennia import DefaultCharacter, Command, default_cmds, DefaultScript, DefaultObject +from evennia.commands.default.help import CmdHelp + +""" +---------------------------------------------------------------------------- +COMBAT FUNCTIONS START HERE +---------------------------------------------------------------------------- +""" + + +def roll_init(character): + """ + Rolls a number between 1-1000 to determine initiative. + + Args: + character (obj): The character to determine initiative for + + Returns: + initiative (int): The character's place in initiative - higher + numbers go first. + + Notes: + By default, does not reference the character and simply returns + a random integer from 1 to 1000. + + Since the character is passed to this function, you can easily reference + a character's stats to determine an initiative roll - for example, if your + character has a 'dexterity' attribute, you can use it to give that character + an advantage in turn order, like so: + + return (randint(1,20)) + character.db.dexterity + + This way, characters with a higher dexterity will go first more often. + """ + return randint(1, 1000) + + +def get_attack(attacker, defender): + """ + Returns a value for an attack roll. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being attacked + + Returns: + attack_value (int): Attack roll value, compared against a defense value + to determine whether an attack hits or misses. + + Notes: + In this example, a weapon's accuracy bonus is factored into the attack + roll. Lighter weapons are more accurate but less damaging, and heavier + weapons are less accurate but deal more damage. Of course, you can + change this paradigm completely in your own game. + """ + # Start with a roll from 1 to 100. + attack_value = randint(1, 100) + accuracy_bonus = 0 + # If armed, add weapon's accuracy bonus. + if attacker.db.wielded_weapon: + weapon = attacker.db.wielded_weapon + accuracy_bonus += weapon.db.accuracy_bonus + # If unarmed, use character's unarmed accuracy bonus. + else: + accuracy_bonus += attacker.db.unarmed_accuracy + # Add the accuracy bonus to the attack roll. + attack_value += accuracy_bonus + return attack_value + + +def get_defense(attacker, defender): + """ + Returns a value for defense, which an attack roll must equal or exceed in order + for an attack to hit. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being attacked + + Returns: + defense_value (int): Defense value, compared against an attack roll + to determine whether an attack hits or misses. + + Notes: + Characters are given a default defense value of 50 which can be + modified up or down by armor. In this example, wearing armor actually + makes you a little easier to hit, but reduces incoming damage. + """ + # Start with a defense value of 50 for a 50/50 chance to hit. + defense_value = 50 + # Modify this value based on defender's armor. + if defender.db.worn_armor: + armor = defender.db.worn_armor + defense_value += armor.db.defense_modifier + return defense_value + + +def get_damage(attacker, defender): + """ + Returns a value for damage to be deducted from the defender's HP after abilities + successful hit. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being damaged + + Returns: + damage_value (int): Damage value, which is to be deducted from the defending + character's HP. + + Notes: + Damage is determined by the attacker's wielded weapon, or the attacker's + unarmed damage range if no weapon is wielded. Incoming damage is reduced + by the defender's armor. + """ + damage_value = 0 + # Generate a damage value from wielded weapon if armed + if attacker.db.wielded_weapon: + weapon = attacker.db.wielded_weapon + # Roll between minimum and maximum damage + damage_value = randint(weapon.db.damage_range[0], weapon.db.damage_range[1]) + # Use attacker's unarmed damage otherwise + else: + damage_value = randint(attacker.db.unarmed_damage_range[0], attacker.db.unarmed_damage_range[1]) + # If defender is armored, reduce incoming damage + if defender.db.worn_armor: + armor = defender.db.worn_armor + damage_value -= armor.db.damage_reduction + # Make sure minimum damage is 0 + if damage_value < 0: + damage_value = 0 + return damage_value + + +def apply_damage(defender, damage): + """ + Applies damage to a target, reducing their HP by the damage amount to a + minimum of 0. + + Args: + defender (obj): Character taking damage + damage (int): Amount of damage being taken + """ + defender.db.hp -= damage # Reduce defender's HP by the damage dealt. + # If this reduces it to 0 or less, set HP to 0. + if defender.db.hp <= 0: + defender.db.hp = 0 + + +def resolve_attack(attacker, defender, attack_value=None, defense_value=None): + """ + Resolves an attack and outputs the result. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being attacked + + Notes: + Even though the attack and defense values are calculated + extremely simply, they are separated out into their own functions + so that they are easier to expand upon. + """ + # Get the attacker's weapon type to reference in combat messages. + attackers_weapon = "attack" + if attacker.db.wielded_weapon: + weapon = attacker.db.wielded_weapon + attackers_weapon = weapon.db.weapon_type_name + # Get an attack roll from the attacker. + if not attack_value: + attack_value = get_attack(attacker, defender) + # Get a defense value from the defender. + if not defense_value: + defense_value = get_defense(attacker, defender) + # If the attack value is lower than the defense value, miss. Otherwise, hit. + if attack_value < defense_value: + attacker.location.msg_contents("%s's %s misses %s!" % (attacker, attackers_weapon, defender)) + else: + damage_value = get_damage(attacker, defender) # Calculate damage value. + # Announce damage dealt and apply damage. + if damage_value > 0: + attacker.location.msg_contents("%s's %s strikes %s for %i damage!" % (attacker, attackers_weapon, defender, damage_value)) + else: + attacker.location.msg_contents("%s's %s bounces harmlessly off %s!" % (attacker, attackers_weapon, defender)) + apply_damage(defender, damage_value) + # If defender HP is reduced to 0 or less, announce defeat. + if defender.db.hp <= 0: + attacker.location.msg_contents("%s has been defeated!" % defender) + + +def combat_cleanup(character): + """ + Cleans up all the temporary combat-related attributes on a character. + + Args: + character (obj): Character to have their combat attributes removed + + Notes: + Any attribute whose key begins with 'combat_' is temporary and no + longer needed once a fight ends. + """ + for attr in character.attributes.all(): + if attr.key[:7] == "combat_": # If the attribute name starts with 'combat_'... + character.attributes.remove(key=attr.key) # ...then delete it! + + +def is_in_combat(character): + """ + Returns true if the given character is in combat. + + Args: + character (obj): Character to determine if is in combat or not + + Returns: + (bool): True if in combat or False if not in combat + """ + if character.db.Combat_TurnHandler: + return True + return False + + +def is_turn(character): + """ + Returns true if it's currently the given character's turn in combat. + + Args: + character (obj): Character to determine if it is their turn or not + + Returns: + (bool): True if it is their turn or False otherwise + """ + turnhandler = character.db.Combat_TurnHandler + currentchar = turnhandler.db.fighters[turnhandler.db.turn] + if character == currentchar: + return True + return False + + +def spend_action(character, actions, action_name=None): + """ + Spends a character's available combat actions and checks for end of turn. + + Args: + character (obj): Character spending the action + actions (int) or 'all': Number of actions to spend, or 'all' to spend all actions + + Kwargs: + action_name (str or None): If a string is given, sets character's last action in + combat to provided string + """ + if action_name: + character.db.Combat_LastAction = action_name + if actions == 'all': # If spending all actions + character.db.Combat_ActionsLeft = 0 # Set actions to 0 + else: + character.db.Combat_ActionsLeft -= actions # Use up actions. + if character.db.Combat_ActionsLeft < 0: + character.db.Combat_ActionsLeft = 0 # Can't have fewer than 0 actions + character.db.Combat_TurnHandler.turn_end_check(character) # Signal potential end of turn. + + +""" +---------------------------------------------------------------------------- +TYPECLASSES START HERE +---------------------------------------------------------------------------- +""" + +class TB_Weapon(DefaultObject): + """ + A weapon which can be wielded in combat with the 'wield' command. + """ + def at_object_creation(self): + """ + Called once, when this object is first created. This is the + normal hook to overload for most object types. + """ + self.db.damage_range = (15, 25) # Minimum and maximum damage on hit + self.db.accuracy_bonus = 0 # Bonus to attack rolls (or penalty if negative) + self.db.weapon_type_name = "weapon" # Single word for weapon - I.E. "dagger", "staff", "scimitar" + def at_drop(self, dropper): + """ + Stop being wielded if dropped. + """ + if dropper.db.wielded_weapon == self: + dropper.db.wielded_weapon = None + dropper.location.msg_contents("%s stops wielding %s." % (dropper, self)) + def at_give(self, giver, getter): + """ + Stop being wielded if given. + """ + if giver.db.wielded_weapon == self: + giver.db.wielded_weapon = None + giver.location.msg_contents("%s stops wielding %s." % (giver, self)) + +class TB_Armor(DefaultObject): + """ + A set of armor which can be worn with the 'don' command. + """ + def at_object_creation(self): + """ + Called once, when this object is first created. This is the + normal hook to overload for most object types. + """ + self.db.damage_reduction = 4 # Amount of incoming damage reduced by armor + self.db.defense_modifier = -4 # Amount to modify defense value (pos = harder to hit, neg = easier) + def at_before_drop(self, dropper): + """ + Can't drop in combat. + """ + if is_in_combat(dropper): + dropper.msg("You can't doff armor in a fight!") + return False + return True + def at_drop(self, dropper): + """ + Stop being wielded if dropped. + """ + if dropper.db.worn_armor == self: + dropper.db.worn_armor = None + dropper.location.msg_contents("%s removes %s." % (dropper, self)) + def at_before_give(self, giver, getter): + """ + Can't give away in combat. + """ + if is_in_combat(giver): + dropper.msg("You can't doff armor in a fight!") + return False + return True + def at_give(self, giver, getter): + """ + Stop being wielded if given. + """ + if giver.db.worn_armor == self: + giver.db.worn_armor = None + giver.location.msg_contents("%s removes %s." % (giver, self)) + +class BattleCharacter(DefaultCharacter): + """ + A character able to participate in turn-based combat. Has attributes for current + and maximum HP, and access to combat commands. + """ + + def at_object_creation(self): + """ + Called once, when this object is first created. This is the + normal hook to overload for most object types. + """ + self.db.max_hp = 100 # Set maximum HP to 100 + self.db.hp = self.db.max_hp # Set current HP to maximum + self.db.wielded_weapon = None # Currently used weapon + self.db.worn_armor = None # Currently worn armor + self.db.unarmed_damage_range = (5, 15) # Minimum and maximum unarmed damage + self.db.unarmed_accuracy = 30 # Accuracy bonus for unarmed attacks + + """ + Adds attributes for a character's current and maximum HP. + We're just going to set this value at '100' by default. + + You may want to expand this to include various 'stats' that + can be changed at creation and factor into combat calculations. + """ + + def at_before_move(self, destination): + """ + Called just before starting to move this object to + destination. + + Args: + destination (Object): The object we are moving to + + Returns: + shouldmove (bool): If we should move or not. + + Notes: + If this method returns False/None, the move is cancelled + before it is even started. + + """ + # Keep the character from moving if at 0 HP or in combat. + if is_in_combat(self): + self.msg("You can't exit a room while in combat!") + return False # Returning false keeps the character from moving. + if self.db.HP <= 0: + self.msg("You can't move, you've been defeated!") + return False + return True + + +""" +---------------------------------------------------------------------------- +COMMANDS START HERE +---------------------------------------------------------------------------- +""" + + +class CmdFight(Command): + """ + Starts a fight with everyone in the same room as you. + + Usage: + fight + + When you start a fight, everyone in the room who is able to + fight is added to combat, and a turn order is randomly rolled. + When it's your turn, you can attack other characters. + """ + key = "fight" + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + here = self.caller.location + fighters = [] + + if not self.caller.db.hp: # If you don't have any hp + self.caller.msg("You can't start a fight if you've been defeated!") + return + if is_in_combat(self.caller): # Already in a fight + self.caller.msg("You're already in a fight!") + return + for thing in here.contents: # Test everything in the room to add it to the fight. + if thing.db.HP: # If the object has HP... + fighters.append(thing) # ...then add it to the fight. + if len(fighters) <= 1: # If you're the only able fighter in the room + self.caller.msg("There's nobody here to fight!") + return + if here.db.Combat_TurnHandler: # If there's already a fight going on... + here.msg_contents("%s joins the fight!" % self.caller) + here.db.Combat_TurnHandler.join_fight(self.caller) # Join the fight! + return + here.msg_contents("%s starts a fight!" % self.caller) + # Add a turn handler script to the room, which starts combat. + here.scripts.add("contrib.turnbattle_equip.TurnHandler") + # Remember you'll have to change the path to the script if you copy this code to your own modules! + + +class CmdAttack(Command): + """ + Attacks another character. + + Usage: + attack + + When in a fight, you may attack another character. The attack has + a chance to hit, and if successful, will deal damage. + """ + + key = "attack" + help_category = "combat" + + def func(self): + "This performs the actual command." + "Set the attacker to the caller and the defender to the target." + + if not is_in_combat(self.caller): # If not in combat, can't attack. + self.caller.msg("You can only do that in combat. (see: help fight)") + return + + if not is_turn(self.caller): # If it's not your turn, can't attack. + self.caller.msg("You can only do that on your turn.") + return + + if not self.caller.db.hp: # Can't attack if you have no HP. + self.caller.msg("You can't attack, you've been defeated.") + return + + attacker = self.caller + defender = self.caller.search(self.args) + + if not defender: # No valid target given. + return + + if not defender.db.hp: # Target object has no HP left or to begin with + self.caller.msg("You can't fight that!") + return + + if attacker == defender: # Target and attacker are the same + self.caller.msg("You can't attack yourself!") + return + + "If everything checks out, call the attack resolving function." + resolve_attack(attacker, defender) + spend_action(self.caller, 1, action_name="attack") # Use up one action. + + +class CmdPass(Command): + """ + Passes on your turn. + + Usage: + pass + + When in a fight, you can use this command to end your turn early, even + if there are still any actions you can take. + """ + + key = "pass" + aliases = ["wait", "hold"] + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + if not is_in_combat(self.caller): # Can only pass a turn in combat. + self.caller.msg("You can only do that in combat. (see: help fight)") + return + + if not is_turn(self.caller): # Can only pass if it's your turn. + self.caller.msg("You can only do that on your turn.") + return + + self.caller.location.msg_contents("%s takes no further action, passing the turn." % self.caller) + spend_action(self.caller, 'all', action_name="pass") # Spend all remaining actions. + + +class CmdDisengage(Command): + """ + Passes your turn and attempts to end combat. + + Usage: + disengage + + Ends your turn early and signals that you're trying to end + the fight. If all participants in a fight disengage, the + fight ends. + """ + + key = "disengage" + aliases = ["spare"] + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + if not is_in_combat(self.caller): # If you're not in combat + self.caller.msg("You can only do that in combat. (see: help fight)") + return + + if not is_turn(self.caller): # If it's not your turn + self.caller.msg("You can only do that on your turn.") + return + + self.caller.location.msg_contents("%s disengages, ready to stop fighting." % self.caller) + spend_action(self.caller, 'all', action_name="disengage") # Spend all remaining actions. + """ + The action_name kwarg sets the character's last action to "disengage", which is checked by + the turn handler script to see if all fighters have disengaged. + """ + + +class CmdRest(Command): + """ + Recovers damage. + + Usage: + rest + + Resting recovers your HP to its maximum, but you can only + rest if you're not in a fight. + """ + + key = "rest" + help_category = "combat" + + def func(self): + "This performs the actual command." + + if is_in_combat(self.caller): # If you're in combat + self.caller.msg("You can't rest while you're in combat.") + return + + self.caller.db.hp = self.caller.db.max_hp # Set current HP to maximum + self.caller.location.msg_contents("%s rests to recover HP." % self.caller) + """ + You'll probably want to replace this with your own system for recovering HP. + """ + + +class CmdCombatHelp(CmdHelp): + """ + View help or a list of topics + + Usage: + help + help list + help all + + This will search for help on commands and other + topics related to the game. + """ + # Just like the default help command, but will give quick + # tips on combat when used in a fight with no arguments. + + def func(self): + if is_in_combat(self.caller) and not self.args: # In combat and entered 'help' alone + self.caller.msg("Available combat commands:|/" + + "|wAttack:|n Attack a target, attempting to deal damage.|/" + + "|wPass:|n Pass your turn without further action.|/" + + "|wDisengage:|n End your turn and attempt to end combat.|/") + else: + super(CmdCombatHelp, self).func() # Call the default help command + +class CmdWield(Command): + """ + Wield a weapon you are carrying + + Usage: + wield + + Select a weapon you are carrying to wield in combat. If + you are already wielding another weapon, you will switch + to the weapon you specify instead. Using this command in + combat will spend your action for your turn. Use the + "unwield" command to stop wielding any weapon you are + currently wielding. + """ + + key = "wield" + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + # If in combat, check to see if it's your turn. + if is_in_combat(self.caller): + if not is_turn(self.caller): + self.caller.msg("You can only do that on your turn.") + return + if not self.args: + self.caller.msg("Usage: wield ") + return + weapon = self.caller.search(self.args, candidates=self.caller.contents) + if not weapon: + return + if not weapon.is_typeclass("evennia.contrib.turnbattle.tb_equip.TB_Weapon"): + self.caller.msg("That's not a weapon!") + # Remember to update the path to the weapon typeclass if you move this module! + return + + if not self.caller.db.wielded_weapon: + self.caller.db.wielded_weapon = weapon + self.caller.location.msg_contents("%s wields %s." % (self.caller, weapon)) + else: + old_weapon = self.caller.db.wielded_weapon + self.caller.db.wielded_weapon = weapon + self.caller.location.msg_contents("%s lowers %s and wields %s." % (self.caller, old_weapon, weapon)) + # Spend an action if in combat. + if is_in_combat(self.caller): + spend_action(self.caller, 1, action_name="wield") # Use up one action. + +class CmdUnwield(Command): + """ + Stop wielding a weapon. + + Usage: + unwield + + After using this command, you will stop wielding any + weapon you are currently wielding and become unarmed. + """ + + key = "unwield" + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + # If in combat, check to see if it's your turn. + if is_in_combat(self.caller): + if not is_turn(self.caller): + self.caller.msg("You can only do that on your turn.") + return + if not self.caller.db.wielded_weapon: + self.caller.msg("You aren't wielding a weapon!") + else: + old_weapon = self.caller.db.wielded_weapon + self.caller.db.wielded_weapon = None + self.caller.location.msg_contents("%s lowers %s." % (self.caller, old_weapon)) + +class CmdDon(Command): + """ + Don armor that you are carrying + + Usage: + don + + Select armor to wear in combat. You can't use this + command in the middle of a fight. Use the "doff" + command to remove any armor you are wearing. + """ + + key = "don" + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + # Can't do this in combat + if is_in_combat(self.caller): + self.caller.msg("You can't don armor in a fight!") + return + if not self.args: + self.caller.msg("Usage: don ") + return + armor = self.caller.search(self.args, candidates=self.caller.contents) + if not armor: + return + if not armor.is_typeclass("evennia.contrib.turnbattle.tb_equip.TB_Armor"): + self.caller.msg("That's not armor!") + # Remember to update the path to the armor typeclass if you move this module! + return + + if not self.caller.db.worn_armor: + self.caller.db.worn_armor = armor + self.caller.location.msg_contents("%s dons %s." % (self.caller, armor)) + else: + old_armor = self.caller.db.worn_armor + self.caller.db.worn_armor = armor + self.caller.location.msg_contents("%s removes %s and dons %s." % (self.caller, old_armor, armor)) + +class CmdDoff(Command): + """ + Stop wearing armor. + + Usage: + doff + + After using this command, you will stop wearing any + armor you are currently using and become unarmored. + You can't use this command in combat. + """ + + key = "doff" + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + # Can't do this in combat + if is_in_combat(self.caller): + self.caller.msg("You can't doff armor in a fight!") + return + if not self.caller.db.worn_armor: + self.caller.msg("You aren't wearing any armor!") + else: + old_armor = self.caller.db.worn_armor + self.caller.db.worn_armor = None + self.caller.location.msg_contents("%s removes %s." % (self.caller, old_armor)) + + + +class BattleCmdSet(default_cmds.CharacterCmdSet): + """ + This command set includes all the commmands used in the battle system. + """ + key = "DefaultCharacter" + + def at_cmdset_creation(self): + """ + Populates the cmdset + """ + self.add(CmdFight()) + self.add(CmdAttack()) + self.add(CmdRest()) + self.add(CmdPass()) + self.add(CmdDisengage()) + self.add(CmdCombatHelp()) + self.add(CmdWield()) + self.add(CmdUnwield()) + self.add(CmdDon()) + self.add(CmdDoff()) + + +""" +---------------------------------------------------------------------------- +SCRIPTS START HERE +---------------------------------------------------------------------------- +""" + + +class TurnHandler(DefaultScript): + """ + This is the script that handles the progression of combat through turns. + On creation (when a fight is started) it adds all combat-ready characters + to its roster and then sorts them into a turn order. There can only be one + fight going on in a single room at a time, so the script is assigned to a + room as its object. + + Fights persist until only one participant is left with any HP or all + remaining participants choose to end the combat with the 'disengage' command. + """ + + def at_script_creation(self): + """ + Called once, when the script is created. + """ + self.key = "Combat Turn Handler" + self.interval = 5 # Once every 5 seconds + self.persistent = True + self.db.fighters = [] + + # Add all fighters in the room with at least 1 HP to the combat." + for object in self.obj.contents: + if object.db.hp: + self.db.fighters.append(object) + + # Initialize each fighter for combat + for fighter in self.db.fighters: + self.initialize_for_combat(fighter) + + # Add a reference to this script to the room + self.obj.db.Combat_TurnHandler = self + + # Roll initiative and sort the list of fighters depending on who rolls highest to determine turn order. + # The initiative roll is determined by the roll_init function and can be customized easily. + ordered_by_roll = sorted(self.db.fighters, key=roll_init, reverse=True) + self.db.fighters = ordered_by_roll + + # Announce the turn order. + self.obj.msg_contents("Turn order is: %s " % ", ".join(obj.key for obj in self.db.fighters)) + + # Set up the current turn and turn timeout delay. + self.db.turn = 0 + self.db.timer = 30 # 30 seconds + + def at_stop(self): + """ + Called at script termination. + """ + for fighter in self.db.fighters: + combat_cleanup(fighter) # Clean up the combat attributes for every fighter. + self.obj.db.Combat_TurnHandler = None # Remove reference to turn handler in location + + def at_repeat(self): + """ + Called once every self.interval seconds. + """ + currentchar = self.db.fighters[self.db.turn] # Note the current character in the turn order. + self.db.timer -= self.interval # Count down the timer. + + if self.db.timer <= 0: + # Force current character to disengage if timer runs out. + self.obj.msg_contents("%s's turn timed out!" % currentchar) + spend_action(currentchar, 'all', action_name="disengage") # Spend all remaining actions. + return + elif self.db.timer <= 10 and not self.db.timeout_warning_given: # 10 seconds left + # Warn the current character if they're about to time out. + currentchar.msg("WARNING: About to time out!") + self.db.timeout_warning_given = True + + def initialize_for_combat(self, character): + """ + Prepares a character for combat when starting or entering a fight. + + Args: + character (obj): Character to initialize for combat. + """ + combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case. + character.db.Combat_ActionsLeft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0 + character.db.Combat_TurnHandler = self # Add a reference to this turn handler script to the character + character.db.Combat_LastAction = "null" # Track last action taken in combat + + def start_turn(self, character): + """ + Readies a character for the start of their turn by replenishing their + available actions and notifying them that their turn has come up. + + Args: + character (obj): Character to be readied. + + Notes: + Here, you only get one action per turn, but you might want to allow more than + one per turn, or even grant a number of actions based on a character's + attributes. You can even add multiple different kinds of actions, I.E. actions + separated for movement, by adding "character.db.Combat_MovesLeft = 3" or + something similar. + """ + character.db.Combat_ActionsLeft = 1 # 1 action per turn. + # Prompt the character for their turn and give some information. + character.msg("|wIt's your turn! You have %i HP remaining.|n" % character.db.hp) + + def next_turn(self): + """ + Advances to the next character in the turn order. + """ + + # Check to see if every character disengaged as their last action. If so, end combat. + disengage_check = True + for fighter in self.db.fighters: + if fighter.db.Combat_LastAction != "disengage": # If a character has done anything but disengage + disengage_check = False + if disengage_check: # All characters have disengaged + self.obj.msg_contents("All fighters have disengaged! Combat is over!") + self.stop() # Stop this script and end combat. + return + + # Check to see if only one character is left standing. If so, end combat. + defeated_characters = 0 + for fighter in self.db.fighters: + if fighter.db.HP == 0: + defeated_characters += 1 # Add 1 for every fighter with 0 HP left (defeated) + if defeated_characters == (len(self.db.fighters) - 1): # If only one character isn't defeated + for fighter in self.db.fighters: + if fighter.db.HP != 0: + LastStanding = fighter # Pick the one fighter left with HP remaining + self.obj.msg_contents("Only %s remains! Combat is over!" % LastStanding) + self.stop() # Stop this script and end combat. + return + + # Cycle to the next turn. + currentchar = self.db.fighters[self.db.turn] + self.db.turn += 1 # Go to the next in the turn order. + if self.db.turn > len(self.db.fighters) - 1: + self.db.turn = 0 # Go back to the first in the turn order once you reach the end. + newchar = self.db.fighters[self.db.turn] # Note the new character + self.db.timer = 30 + self.time_until_next_repeat() # Reset the timer. + self.db.timeout_warning_given = False # Reset the timeout warning. + self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar)) + self.start_turn(newchar) # Start the new character's turn. + + def turn_end_check(self, character): + """ + Tests to see if a character's turn is over, and cycles to the next turn if it is. + + Args: + character (obj): Character to test for end of turn + """ + if not character.db.Combat_ActionsLeft: # Character has no actions remaining + self.next_turn() + return + + def join_fight(self, character): + """ + Adds a new character to a fight already in progress. + + Args: + character (obj): Character to be added to the fight. + """ + # Inserts the fighter to the turn order, right behind whoever's turn it currently is. + self.db.fighters.insert(self.db.turn, character) + # Tick the turn counter forward one to compensate. + self.db.turn += 1 + # Initialize the character like you do at the start. + self.initialize_for_combat(character) + +""" +---------------------------------------------------------------------------- +PROTOTYPES START HERE +---------------------------------------------------------------------------- +""" + +BASEWEAPON = { + "typeclass": "evennia.contrib.turnbattle.tb_equip.TB_Weapon", +} + +BASEARMOR = { + "typeclass": "evennia.contrib.turnbattle.tb_equip.TB_Armor", +} + +DAGGER = { + "prototype" : "BASEWEAPON", + "damage_range" : (10, 20), + "accuracy_bonus" : 30, + "key": "a thin steel dagger", + "weapon_type_name" : "dagger" +} + +BROADSWORD = { + "prototype" : "BASEWEAPON", + "damage_range" : (15, 30), + "accuracy_bonus" : 15, + "key": "an iron broadsword", + "weapon_type_name" : "broadsword" +} + +GREATSWORD = { + "prototype" : "BASEWEAPON", + "damage_range" : (20, 40), + "accuracy_bonus" : 0, + "key": "a rune-etched greatsword", + "weapon_type_name" : "greatsword" +} + +LEATHERARMOR = { + "prototype" : "BASEARMOR", + "damage_reduction" : 2, + "defense_modifier" : -2, + "key": "a suit of leather armor" +} + +SCALEMAIL = { + "prototype" : "BASEARMOR", + "damage_reduction" : 4, + "defense_modifier" : -4, + "key": "a suit of scale mail" +} + +PLATEMAIL = { + "prototype" : "BASEARMOR", + "damage_reduction" : 6, + "defense_modifier" : -6, + "key": "a suit of plate mail" +} From d4d8a9c1b8240c86311206202ed9dcbbdb76bada Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 8 Oct 2017 20:07:29 -0700 Subject: [PATCH 11/68] Readme for turnbattle folder --- evennia/contrib/turnbattle/README.md | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 evennia/contrib/turnbattle/README.md diff --git a/evennia/contrib/turnbattle/README.md b/evennia/contrib/turnbattle/README.md new file mode 100644 index 0000000000..8d709d1ba1 --- /dev/null +++ b/evennia/contrib/turnbattle/README.md @@ -0,0 +1,29 @@ +# Turn based battle system framework + +Contrib - Tim Ashley Jenkins 2017 + +This is a framework for a simple turn-based combat system, similar +to those used in D&D-style tabletop role playing games. It allows +any character to start a fight in a room, at which point initiative +is rolled and a turn order is established. Each participant in combat +has a limited time to decide their action for that turn (30 seconds by +default), and combat progresses through the turn order, looping through +the participants until the fight ends. + +This folder contains multiple examples of how such a system can be +implemented and customized: + + tb_basic.py - The simplest system, which implements initiative and turn + order, attack rolls against defense values, and damage to hit + points. Only very basic game mechanics are included. + + tb_equip.py - Adds weapons and armor to the basic implementation of + the battle system, including commands for wielding weapons and + donning armor, and modifiers to accuracy and damage based on + currently used equipment. + +This system is meant as a basic framework to start from, and is modeled +after the combat systems of popular tabletop role playing games rather than +the real-time battle systems that many MMOs and some MUDs use. As such, it +may be better suited to role-playing or more story-oriented games, or games +meant to closely emulate the experience of playing a tabletop RPG. From b50c7a1f3e3d87bc0c0d23faa2dc96eb7ce28d16 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 8 Oct 2017 20:08:08 -0700 Subject: [PATCH 12/68] __init__.py for Turnbattle folder --- evennia/contrib/turnbattle/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 evennia/contrib/turnbattle/__init__.py diff --git a/evennia/contrib/turnbattle/__init__.py b/evennia/contrib/turnbattle/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/evennia/contrib/turnbattle/__init__.py @@ -0,0 +1 @@ + From a3fd45bebbc13d373ef20fc42be23d9bfeca2ba0 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 8 Oct 2017 20:09:30 -0700 Subject: [PATCH 13/68] Update contrib unit tests for turnbattle Points the contrib unit tests to the turnbattle module's new location in its subfolder. --- evennia/contrib/tests.py | 42 +++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 1678d06567..20a24eb177 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -907,7 +907,7 @@ class TestTutorialWorldRooms(CommandTest): # test turnbattle -from evennia.contrib import turnbattle +from evennia.contrib.turnbattle import tb_basic from evennia.objects.objects import DefaultRoom @@ -915,60 +915,59 @@ class TestTurnBattleCmd(CommandTest): # Test combat commands def test_turnbattlecmd(self): - self.call(turnbattle.CmdFight(), "", "You can't start a fight if you've been defeated!") - self.call(turnbattle.CmdAttack(), "", "You can only do that in combat. (see: help fight)") - self.call(turnbattle.CmdPass(), "", "You can only do that in combat. (see: help fight)") - self.call(turnbattle.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") - self.call(turnbattle.CmdRest(), "", "Char rests to recover HP.") - + self.call(tb_basic.CmdFight(), "", "You can't start a fight if you've been defeated!") + self.call(tb_basic.CmdAttack(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_basic.CmdPass(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_basic.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_basic.CmdRest(), "", "Char rests to recover HP.") class TestTurnBattleFunc(EvenniaTest): # Test combat functions def test_turnbattlefunc(self): - attacker = create_object(turnbattle.BattleCharacter, key="Attacker") - defender = create_object(turnbattle.BattleCharacter, key="Defender") + attacker = create_object(tb_basic.BattleCharacter, key="Attacker") + defender = create_object(tb_basic.BattleCharacter, key="Defender") testroom = create_object(DefaultRoom, key="Test Room") attacker.location = testroom defender.loaction = testroom # Initiative roll - initiative = turnbattle.roll_init(attacker) + initiative = tb_basic.roll_init(attacker) self.assertTrue(initiative >= 0 and initiative <= 1000) # Attack roll - attack_roll = turnbattle.get_attack(attacker, defender) + attack_roll = tb_basic.get_attack(attacker, defender) self.assertTrue(attack_roll >= 0 and attack_roll <= 100) # Defense roll - defense_roll = turnbattle.get_defense(attacker, defender) + defense_roll = tb_basic.get_defense(attacker, defender) self.assertTrue(defense_roll == 50) # Damage roll - damage_roll = turnbattle.get_damage(attacker, defender) + damage_roll = tb_basic.get_damage(attacker, defender) self.assertTrue(damage_roll >= 15 and damage_roll <= 25) # Apply damage defender.db.hp = 10 - turnbattle.apply_damage(defender, 3) + tb_basic.apply_damage(defender, 3) self.assertTrue(defender.db.hp == 7) # Resolve attack defender.db.hp = 40 - turnbattle.resolve_attack(attacker, defender, attack_value=20, defense_value=10) + tb_basic.resolve_attack(attacker, defender, attack_value=20, defense_value=10) self.assertTrue(defender.db.hp < 40) # Combat cleanup attacker.db.Combat_attribute = True - turnbattle.combat_cleanup(attacker) + tb_basic.combat_cleanup(attacker) self.assertFalse(attacker.db.combat_attribute) # Is in combat - self.assertFalse(turnbattle.is_in_combat(attacker)) + self.assertFalse(tb_basic.is_in_combat(attacker)) # Set up turn handler script for further tests - attacker.location.scripts.add(turnbattle.TurnHandler) + attacker.location.scripts.add(tb_basic.TurnHandler) turnhandler = attacker.db.combat_TurnHandler self.assertTrue(attacker.db.combat_TurnHandler) # Force turn order turnhandler.db.fighters = [attacker, defender] turnhandler.db.turn = 0 # Test is turn - self.assertTrue(turnbattle.is_turn(attacker)) + self.assertTrue(tb_basic.is_turn(attacker)) # Spend actions attacker.db.Combat_ActionsLeft = 1 - turnbattle.spend_action(attacker, 1, action_name="Test") + tb_basic.spend_action(attacker, 1, action_name="Test") self.assertTrue(attacker.db.Combat_ActionsLeft == 0) self.assertTrue(attacker.db.Combat_LastAction == "Test") # Initialize for combat @@ -992,7 +991,7 @@ class TestTurnBattleFunc(EvenniaTest): turnhandler.turn_end_check(attacker) self.assertTrue(turnhandler.db.turn == 1) # Join fight - joiner = create_object(turnbattle.BattleCharacter, key="Joiner") + joiner = create_object(tb_basic.BattleCharacter, key="Joiner") turnhandler.db.fighters = [attacker, defender] turnhandler.db.turn = 0 turnhandler.join_fight(joiner) @@ -1001,7 +1000,6 @@ class TestTurnBattleFunc(EvenniaTest): # Remove the script at the end turnhandler.stop() - # Test of the unixcommand module from evennia.contrib.unixcommand import UnixCommand From f031ba1b21721edb89b36ee115b49b185a8d8d1c Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 9 Oct 2017 00:13:03 -0700 Subject: [PATCH 14/68] Renamed typeclasses to avoid conflicts --- evennia/contrib/turnbattle/tb_basic.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_basic.py b/evennia/contrib/turnbattle/tb_basic.py index f1ff33a483..70c81debae 100644 --- a/evennia/contrib/turnbattle/tb_basic.py +++ b/evennia/contrib/turnbattle/tb_basic.py @@ -16,15 +16,15 @@ is easily extensible and can be used as the foundation for implementing the rules from your turn-based tabletop game of choice or making your own battle system. -To install and test, import this module's BattleCharacter object into +To install and test, import this module's TBBasicCharacter object into your game's character.py module: - from evennia.contrib.turnbattle.tb_basic import BattleCharacter + from evennia.contrib.turnbattle.tb_basic import TBBasicCharacter -And change your game's character typeclass to inherit from BattleCharacter +And change your game's character typeclass to inherit from TBBasicCharacter instead of the default: - class Character(BattleCharacter): + class Character(TBBasicCharacter): Next, import this module into your default_cmdsets.py module: @@ -278,7 +278,7 @@ CHARACTER TYPECLASS """ -class BattleCharacter(DefaultCharacter): +class TBBasicCharacter(DefaultCharacter): """ A character able to participate in turn-based combat. Has attributes for current and maximum HP, and access to combat commands. @@ -371,7 +371,7 @@ class CmdFight(Command): return here.msg_contents("%s starts a fight!" % self.caller) # Add a turn handler script to the room, which starts combat. - here.scripts.add("contrib.turnbattle.tb_basic.TurnHandler") + here.scripts.add("contrib.turnbattle.tb_basic.TBBasicTurnHandler") # Remember you'll have to change the path to the script if you copy this code to your own modules! @@ -569,7 +569,7 @@ SCRIPTS START HERE """ -class TurnHandler(DefaultScript): +class TBBasicTurnHandler(DefaultScript): """ This is the script that handles the progression of combat through turns. On creation (when a fight is started) it adds all combat-ready characters From da8b20e0b19cd46dd52cf66b69974eceb1fedfdb Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 9 Oct 2017 00:14:32 -0700 Subject: [PATCH 15/68] Renamed typeclasses to avoid conflicts --- evennia/contrib/turnbattle/tb_equip.py | 29 ++++++++++++++------------ 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_equip.py b/evennia/contrib/turnbattle/tb_equip.py index 702d02520e..7d9ea58442 100644 --- a/evennia/contrib/turnbattle/tb_equip.py +++ b/evennia/contrib/turnbattle/tb_equip.py @@ -28,15 +28,15 @@ expanded to fit your game - weapon and armor slots, damage types and damage bonuses, etc. should be fairly simple to implement according to the rules of your preferred system or the needs of your own game. -To install and test, import this module's BattleCharacter object into +To install and test, import this module's TBEquipCharacter object into your game's character.py module: - from evennia.contrib.turnbattle.tb_equip import BattleCharacter + from evennia.contrib.turnbattle.tb_equip import TBEquipCharacter -And change your game's character typeclass to inherit from BattleCharacter +And change your game's character typeclass to inherit from TBEquipCharacter instead of the default: - class Character(BattleCharacter): + class Character(TBEquipCharacter): Next, import this module into your default_cmdsets.py module: @@ -321,7 +321,7 @@ TYPECLASSES START HERE ---------------------------------------------------------------------------- """ -class TB_Weapon(DefaultObject): +class TBEWeapon(DefaultObject): """ A weapon which can be wielded in combat with the 'wield' command. """ @@ -348,7 +348,7 @@ class TB_Weapon(DefaultObject): giver.db.wielded_weapon = None giver.location.msg_contents("%s stops wielding %s." % (giver, self)) -class TB_Armor(DefaultObject): +class TBEArmor(DefaultObject): """ A set of armor which can be worn with the 'don' command. """ @@ -390,7 +390,7 @@ class TB_Armor(DefaultObject): giver.db.worn_armor = None giver.location.msg_contents("%s removes %s." % (giver, self)) -class BattleCharacter(DefaultCharacter): +class TBEquipCharacter(DefaultCharacter): """ A character able to participate in turn-based combat. Has attributes for current and maximum HP, and access to combat commands. @@ -488,7 +488,7 @@ class CmdFight(Command): return here.msg_contents("%s starts a fight!" % self.caller) # Add a turn handler script to the room, which starts combat. - here.scripts.add("contrib.turnbattle_equip.TurnHandler") + here.scripts.add("contrib.turnbattle.tb_equip.TBEquipTurnHandler") # Remember you'll have to change the path to the script if you copy this code to your own modules! @@ -693,7 +693,7 @@ class CmdWield(Command): weapon = self.caller.search(self.args, candidates=self.caller.contents) if not weapon: return - if not weapon.is_typeclass("evennia.contrib.turnbattle.tb_equip.TB_Weapon"): + if not weapon.is_typeclass("evennia.contrib.turnbattle.tb_equip.TBEWeapon"): self.caller.msg("That's not a weapon!") # Remember to update the path to the weapon typeclass if you move this module! return @@ -768,7 +768,7 @@ class CmdDon(Command): armor = self.caller.search(self.args, candidates=self.caller.contents) if not armor: return - if not armor.is_typeclass("evennia.contrib.turnbattle.tb_equip.TB_Armor"): + if not armor.is_typeclass("evennia.contrib.turnbattle.tb_equip.TBEArmor"): self.caller.msg("That's not armor!") # Remember to update the path to the armor typeclass if you move this module! return @@ -842,7 +842,7 @@ SCRIPTS START HERE """ -class TurnHandler(DefaultScript): +class TBEquipTurnHandler(DefaultScript): """ This is the script that handles the progression of combat through turns. On creation (when a fight is started) it adds all combat-ready characters @@ -882,6 +882,9 @@ class TurnHandler(DefaultScript): # Announce the turn order. self.obj.msg_contents("Turn order is: %s " % ", ".join(obj.key for obj in self.db.fighters)) + + # Start first fighter's turn. + self.start_turn(self.db.fighters[0]) # Set up the current turn and turn timeout delay. self.db.turn = 0 @@ -1014,11 +1017,11 @@ PROTOTYPES START HERE """ BASEWEAPON = { - "typeclass": "evennia.contrib.turnbattle.tb_equip.TB_Weapon", + "typeclass": "evennia.contrib.turnbattle.tb_equip.TBEWeapon", } BASEARMOR = { - "typeclass": "evennia.contrib.turnbattle.tb_equip.TB_Armor", + "typeclass": "evennia.contrib.turnbattle.tb_equip.TBEArmor", } DAGGER = { From 576e2b4be6985f88082fe907c3053aed03492e4f Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 9 Oct 2017 00:16:14 -0700 Subject: [PATCH 16/68] Updated typeclass names & added tests for tb_equip commands --- evennia/contrib/tests.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 20a24eb177..f611471c0f 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -907,7 +907,7 @@ class TestTutorialWorldRooms(CommandTest): # test turnbattle -from evennia.contrib.turnbattle import tb_basic +from evennia.contrib.turnbattle import tb_basic, tb_equip from evennia.objects.objects import DefaultRoom @@ -920,13 +920,32 @@ class TestTurnBattleCmd(CommandTest): self.call(tb_basic.CmdPass(), "", "You can only do that in combat. (see: help fight)") self.call(tb_basic.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") self.call(tb_basic.CmdRest(), "", "Char rests to recover HP.") + + # Test equipment commands + def test_turnbattleequipcmd(self): + # Start with equip module specific commands. + testweapon = create_object(tb_equip.TBEWeapon, key="test weapon") + testarmor = create_object(tb_equip.TBEArmor, key="test armor") + testweapon.move_to(self.char1) + testarmor.move_to(self.char1) + self.call(tb_equip.CmdWield(), "weapon", "Char wields test weapon.") + self.call(tb_equip.CmdUnwield(), "", "Char lowers test weapon.") + self.call(tb_equip.CmdDon(), "armor", "Char dons test armor.") + self.call(tb_equip.CmdDoff(), "", "Char removes test armor.") + # Also test the commands that are the same in the basic module + self.call(tb_equip.CmdFight(), "", "You can't start a fight if you've been defeated!") + self.call(tb_equip.CmdAttack(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_equip.CmdPass(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_equip.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_equip.CmdRest(), "", "Char rests to recover HP.") + class TestTurnBattleFunc(EvenniaTest): # Test combat functions def test_turnbattlefunc(self): - attacker = create_object(tb_basic.BattleCharacter, key="Attacker") - defender = create_object(tb_basic.BattleCharacter, key="Defender") + attacker = create_object(tb_basic.TBBasicCharacter, key="Attacker") + defender = create_object(tb_basic.TBBasicCharacter, key="Defender") testroom = create_object(DefaultRoom, key="Test Room") attacker.location = testroom defender.loaction = testroom @@ -957,7 +976,7 @@ class TestTurnBattleFunc(EvenniaTest): # Is in combat self.assertFalse(tb_basic.is_in_combat(attacker)) # Set up turn handler script for further tests - attacker.location.scripts.add(tb_basic.TurnHandler) + attacker.location.scripts.add(tb_basic.TBBasicTurnHandler) turnhandler = attacker.db.combat_TurnHandler self.assertTrue(attacker.db.combat_TurnHandler) # Force turn order @@ -991,7 +1010,7 @@ class TestTurnBattleFunc(EvenniaTest): turnhandler.turn_end_check(attacker) self.assertTrue(turnhandler.db.turn == 1) # Join fight - joiner = create_object(tb_basic.BattleCharacter, key="Joiner") + joiner = create_object(tb_basic.TBBasicCharacter, key="Joiner") turnhandler.db.fighters = [attacker, defender] turnhandler.db.turn = 0 turnhandler.join_fight(joiner) @@ -1000,6 +1019,7 @@ class TestTurnBattleFunc(EvenniaTest): # Remove the script at the end turnhandler.stop() + # Test of the unixcommand module from evennia.contrib.unixcommand import UnixCommand From 8bdfa011fbe2e50f1c9c27105408430ce2e807f4 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 9 Oct 2017 17:44:47 -0700 Subject: [PATCH 17/68] Added more tb_equip tests The unit tests for tb_basic and tb_equip are almost the same, with a few minor differences created by the different default values for unarmed attack and damage rolls. --- evennia/contrib/tests.py | 77 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index f611471c0f..1fdd7dde75 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1018,6 +1018,83 @@ class TestTurnBattleFunc(EvenniaTest): self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender]) # Remove the script at the end turnhandler.stop() + + # Test the combat functions in tb_equip too. They work mostly the same. + def test_turnbattlefunc(self): + attacker = create_object(tb_equip.TBEquipCharacter, key="Attacker") + defender = create_object(tb_equip.TBEquipCharacter, key="Defender") + testroom = create_object(DefaultRoom, key="Test Room") + attacker.location = testroom + defender.loaction = testroom + # Initiative roll + initiative = tb_equip.roll_init(attacker) + self.assertTrue(initiative >= 0 and initiative <= 1000) + # Attack roll + attack_roll = tb_equip.get_attack(attacker, defender) + self.assertTrue(attack_roll >= -50 and attack_roll <= 150) + # Defense roll + defense_roll = tb_equip.get_defense(attacker, defender) + self.assertTrue(defense_roll == 50) + # Damage roll + damage_roll = tb_equip.get_damage(attacker, defender) + self.assertTrue(damage_roll >= 0 and damage_roll <= 50) + # Apply damage + defender.db.hp = 10 + tb_equip.apply_damage(defender, 3) + self.assertTrue(defender.db.hp == 7) + # Resolve attack + defender.db.hp = 40 + tb_equip.resolve_attack(attacker, defender, attack_value=20, defense_value=10) + self.assertTrue(defender.db.hp < 40) + # Combat cleanup + attacker.db.Combat_attribute = True + tb_equip.combat_cleanup(attacker) + self.assertFalse(attacker.db.combat_attribute) + # Is in combat + self.assertFalse(tb_equip.is_in_combat(attacker)) + # Set up turn handler script for further tests + attacker.location.scripts.add(tb_equip.TBEquipTurnHandler) + turnhandler = attacker.db.combat_TurnHandler + self.assertTrue(attacker.db.combat_TurnHandler) + # Force turn order + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + # Test is turn + self.assertTrue(tb_equip.is_turn(attacker)) + # Spend actions + attacker.db.Combat_ActionsLeft = 1 + tb_equip.spend_action(attacker, 1, action_name="Test") + self.assertTrue(attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(attacker.db.Combat_LastAction == "Test") + # Initialize for combat + attacker.db.Combat_ActionsLeft = 983 + turnhandler.initialize_for_combat(attacker) + self.assertTrue(attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(attacker.db.Combat_LastAction == "null") + # Start turn + defender.db.Combat_ActionsLeft = 0 + turnhandler.start_turn(defender) + self.assertTrue(defender.db.Combat_ActionsLeft == 1) + # Next turn + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + turnhandler.next_turn() + self.assertTrue(turnhandler.db.turn == 1) + # Turn end check + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + attacker.db.Combat_ActionsLeft = 0 + turnhandler.turn_end_check(attacker) + self.assertTrue(turnhandler.db.turn == 1) + # Join fight + joiner = create_object(tb_equip.TBEquipCharacter, key="Joiner") + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + turnhandler.join_fight(joiner) + self.assertTrue(turnhandler.db.turn == 1) + self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender]) + # Remove the script at the end + turnhandler.stop() # Test of the unixcommand module From 3e02f96566dab3aff233e23fb52b9ee1c04b5f42 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 14 Oct 2017 20:15:44 +0200 Subject: [PATCH 18/68] Add ability to search_script by typeclass --- evennia/scripts/manager.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/evennia/scripts/manager.py b/evennia/scripts/manager.py index 17a11d20b3..1b795b2c3e 100644 --- a/evennia/scripts/manager.py +++ b/evennia/scripts/manager.py @@ -214,7 +214,7 @@ class ScriptDBManager(TypedObjectManager): VALIDATE_ITERATION -= 1 return nr_started, nr_stopped - def search_script(self, ostring, obj=None, only_timed=False): + def search_script(self, ostring, obj=None, only_timed=False, typeclass=None): """ Search for a particular script. @@ -224,6 +224,7 @@ class ScriptDBManager(TypedObjectManager): this object only_timed (bool): Limit search only to scripts that run on a timer. + typeclass (class or str): Typeclass or path to typeclass. """ @@ -237,10 +238,17 @@ class ScriptDBManager(TypedObjectManager): (only_timed and dbref_match.interval)): return [dbref_match] + if typeclass: + if callable(typeclass): + typeclass = u"%s.%s" % (typeclass.__module__, typeclass.__name__) + else: + typeclass = u"%s" % typeclass + # not a dbref; normal search obj_restriction = obj and Q(db_obj=obj) or Q() - timed_restriction = only_timed and Q(interval__gt=0) or Q() - scripts = self.filter(timed_restriction & obj_restriction & Q(db_key__iexact=ostring)) + timed_restriction = only_timed and Q(db_interval__gt=0) or Q() + typeclass_restriction = typeclass and Q(db_typeclass_path=typeclass) or Q() + scripts = self.filter(timed_restriction & obj_restriction & typeclass_restriction & Q(db_key__iexact=ostring)) return scripts # back-compatibility alias script_search = search_script From 6cbd9984d71eb08a33340208857a6c2411144fc6 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sat, 14 Oct 2017 22:31:10 -0700 Subject: [PATCH 19/68] Delete turnbattle.py Now that this system has been moved to /turnbattle/, this file is obsolete. --- evennia/contrib/turnbattle.py | 735 ---------------------------------- 1 file changed, 735 deletions(-) delete mode 100644 evennia/contrib/turnbattle.py diff --git a/evennia/contrib/turnbattle.py b/evennia/contrib/turnbattle.py deleted file mode 100644 index 692656bc3a..0000000000 --- a/evennia/contrib/turnbattle.py +++ /dev/null @@ -1,735 +0,0 @@ -""" -Simple turn-based combat system - -Contrib - Tim Ashley Jenkins 2017 - -This is a framework for a simple turn-based combat system, similar -to those used in D&D-style tabletop role playing games. It allows -any character to start a fight in a room, at which point initiative -is rolled and a turn order is established. Each participant in combat -has a limited time to decide their action for that turn (30 seconds by -default), and combat progresses through the turn order, looping through -the participants until the fight ends. - -Only simple rolls for attacking are implemented here, but this system -is easily extensible and can be used as the foundation for implementing -the rules from your turn-based tabletop game of choice or making your -own battle system. - -To install and test, import this module's BattleCharacter object into -your game's character.py module: - - from evennia.contrib.turnbattle import BattleCharacter - -And change your game's character typeclass to inherit from BattleCharacter -instead of the default: - - class Character(BattleCharacter): - -Next, import this module into your default_cmdsets.py module: - - from evennia.contrib import turnbattle - -And add the battle command set to your default command set: - - # - # any commands you add below will overload the default ones. - # - self.add(turnbattle.BattleCmdSet()) - -This module is meant to be heavily expanded on, so you may want to copy it -to your game's 'world' folder and modify it there rather than importing it -in your game and using it as-is. -""" - -from random import randint -from evennia import DefaultCharacter, Command, default_cmds, DefaultScript -from evennia.commands.default.help import CmdHelp - -""" ----------------------------------------------------------------------------- -COMBAT FUNCTIONS START HERE ----------------------------------------------------------------------------- -""" - - -def roll_init(character): - """ - Rolls a number between 1-1000 to determine initiative. - - Args: - character (obj): The character to determine initiative for - - Returns: - initiative (int): The character's place in initiative - higher - numbers go first. - - Notes: - By default, does not reference the character and simply returns - a random integer from 1 to 1000. - - Since the character is passed to this function, you can easily reference - a character's stats to determine an initiative roll - for example, if your - character has a 'dexterity' attribute, you can use it to give that character - an advantage in turn order, like so: - - return (randint(1,20)) + character.db.dexterity - - This way, characters with a higher dexterity will go first more often. - """ - return randint(1, 1000) - - -def get_attack(attacker, defender): - """ - Returns a value for an attack roll. - - Args: - attacker (obj): Character doing the attacking - defender (obj): Character being attacked - - Returns: - attack_value (int): Attack roll value, compared against a defense value - to determine whether an attack hits or misses. - - Notes: - By default, returns a random integer from 1 to 100 without using any - properties from either the attacker or defender. - - This can easily be expanded to return a value based on characters stats, - equipment, and abilities. This is why the attacker and defender are passed - to this function, even though nothing from either one are used in this example. - """ - # For this example, just return a random integer up to 100. - attack_value = randint(1, 100) - return attack_value - - -def get_defense(attacker, defender): - """ - Returns a value for defense, which an attack roll must equal or exceed in order - for an attack to hit. - - Args: - attacker (obj): Character doing the attacking - defender (obj): Character being attacked - - Returns: - defense_value (int): Defense value, compared against an attack roll - to determine whether an attack hits or misses. - - Notes: - By default, returns 50, not taking any properties of the defender or - attacker into account. - - As above, this can be expanded upon based on character stats and equipment. - """ - # For this example, just return 50, for about a 50/50 chance of hit. - defense_value = 50 - return defense_value - - -def get_damage(attacker, defender): - """ - Returns a value for damage to be deducted from the defender's HP after abilities - successful hit. - - Args: - attacker (obj): Character doing the attacking - defender (obj): Character being damaged - - Returns: - damage_value (int): Damage value, which is to be deducted from the defending - character's HP. - - Notes: - By default, returns a random integer from 15 to 25 without using any - properties from either the attacker or defender. - - Again, this can be expanded upon. - """ - # For this example, just generate a number between 15 and 25. - damage_value = randint(15, 25) - return damage_value - - -def apply_damage(defender, damage): - """ - Applies damage to a target, reducing their HP by the damage amount to a - minimum of 0. - - Args: - defender (obj): Character taking damage - damage (int): Amount of damage being taken - """ - defender.db.hp -= damage # Reduce defender's HP by the damage dealt. - # If this reduces it to 0 or less, set HP to 0. - if defender.db.hp <= 0: - defender.db.hp = 0 - - -def resolve_attack(attacker, defender, attack_value=None, defense_value=None): - """ - Resolves an attack and outputs the result. - - Args: - attacker (obj): Character doing the attacking - defender (obj): Character being attacked - - Notes: - Even though the attack and defense values are calculated - extremely simply, they are separated out into their own functions - so that they are easier to expand upon. - """ - # Get an attack roll from the attacker. - if not attack_value: - attack_value = get_attack(attacker, defender) - # Get a defense value from the defender. - if not defense_value: - defense_value = get_defense(attacker, defender) - # If the attack value is lower than the defense value, miss. Otherwise, hit. - if attack_value < defense_value: - attacker.location.msg_contents("%s's attack misses %s!" % (attacker, defender)) - else: - damage_value = get_damage(attacker, defender) # Calculate damage value. - # Announce damage dealt and apply damage. - attacker.location.msg_contents("%s hits %s for %i damage!" % (attacker, defender, damage_value)) - apply_damage(defender, damage_value) - # If defender HP is reduced to 0 or less, announce defeat. - if defender.db.hp <= 0: - attacker.location.msg_contents("%s has been defeated!" % defender) - - -def combat_cleanup(character): - """ - Cleans up all the temporary combat-related attributes on a character. - - Args: - character (obj): Character to have their combat attributes removed - - Notes: - Any attribute whose key begins with 'combat_' is temporary and no - longer needed once a fight ends. - """ - for attr in character.attributes.all(): - if attr.key[:7] == "combat_": # If the attribute name starts with 'combat_'... - character.attributes.remove(key=attr.key) # ...then delete it! - - -def is_in_combat(character): - """ - Returns true if the given character is in combat. - - Args: - character (obj): Character to determine if is in combat or not - - Returns: - (bool): True if in combat or False if not in combat - """ - if character.db.Combat_TurnHandler: - return True - return False - - -def is_turn(character): - """ - Returns true if it's currently the given character's turn in combat. - - Args: - character (obj): Character to determine if it is their turn or not - - Returns: - (bool): True if it is their turn or False otherwise - """ - turnhandler = character.db.Combat_TurnHandler - currentchar = turnhandler.db.fighters[turnhandler.db.turn] - if character == currentchar: - return True - return False - - -def spend_action(character, actions, action_name=None): - """ - Spends a character's available combat actions and checks for end of turn. - - Args: - character (obj): Character spending the action - actions (int) or 'all': Number of actions to spend, or 'all' to spend all actions - - Kwargs: - action_name (str or None): If a string is given, sets character's last action in - combat to provided string - """ - if action_name: - character.db.Combat_LastAction = action_name - if actions == 'all': # If spending all actions - character.db.Combat_ActionsLeft = 0 # Set actions to 0 - else: - character.db.Combat_ActionsLeft -= actions # Use up actions. - if character.db.Combat_ActionsLeft < 0: - character.db.Combat_ActionsLeft = 0 # Can't have fewer than 0 actions - character.db.Combat_TurnHandler.turn_end_check(character) # Signal potential end of turn. - - -""" ----------------------------------------------------------------------------- -CHARACTER TYPECLASS ----------------------------------------------------------------------------- -""" - - -class BattleCharacter(DefaultCharacter): - """ - A character able to participate in turn-based combat. Has attributes for current - and maximum HP, and access to combat commands. - """ - - def at_object_creation(self): - """ - Called once, when this object is first created. This is the - normal hook to overload for most object types. - """ - self.db.max_hp = 100 # Set maximum HP to 100 - self.db.hp = self.db.max_hp # Set current HP to maximum - """ - Adds attributes for a character's current and maximum HP. - We're just going to set this value at '100' by default. - - You may want to expand this to include various 'stats' that - can be changed at creation and factor into combat calculations. - """ - - def at_before_move(self, destination): - """ - Called just before starting to move this object to - destination. - - Args: - destination (Object): The object we are moving to - - Returns: - shouldmove (bool): If we should move or not. - - Notes: - If this method returns False/None, the move is cancelled - before it is even started. - - """ - # Keep the character from moving if at 0 HP or in combat. - if is_in_combat(self): - self.msg("You can't exit a room while in combat!") - return False # Returning false keeps the character from moving. - if self.db.HP <= 0: - self.msg("You can't move, you've been defeated!") - return False - return True - - -""" ----------------------------------------------------------------------------- -COMMANDS START HERE ----------------------------------------------------------------------------- -""" - - -class CmdFight(Command): - """ - Starts a fight with everyone in the same room as you. - - Usage: - fight - - When you start a fight, everyone in the room who is able to - fight is added to combat, and a turn order is randomly rolled. - When it's your turn, you can attack other characters. - """ - key = "fight" - help_category = "combat" - - def func(self): - """ - This performs the actual command. - """ - here = self.caller.location - fighters = [] - - if not self.caller.db.hp: # If you don't have any hp - self.caller.msg("You can't start a fight if you've been defeated!") - return - if is_in_combat(self.caller): # Already in a fight - self.caller.msg("You're already in a fight!") - return - for thing in here.contents: # Test everything in the room to add it to the fight. - if thing.db.HP: # If the object has HP... - fighters.append(thing) # ...then add it to the fight. - if len(fighters) <= 1: # If you're the only able fighter in the room - self.caller.msg("There's nobody here to fight!") - return - if here.db.Combat_TurnHandler: # If there's already a fight going on... - here.msg_contents("%s joins the fight!" % self.caller) - here.db.Combat_TurnHandler.join_fight(self.caller) # Join the fight! - return - here.msg_contents("%s starts a fight!" % self.caller) - # Add a turn handler script to the room, which starts combat. - here.scripts.add("contrib.turnbattle.TurnHandler") - # Remember you'll have to change the path to the script if you copy this code to your own modules! - - -class CmdAttack(Command): - """ - Attacks another character. - - Usage: - attack - - When in a fight, you may attack another character. The attack has - a chance to hit, and if successful, will deal damage. - """ - - key = "attack" - help_category = "combat" - - def func(self): - "This performs the actual command." - "Set the attacker to the caller and the defender to the target." - - if not is_in_combat(self.caller): # If not in combat, can't attack. - self.caller.msg("You can only do that in combat. (see: help fight)") - return - - if not is_turn(self.caller): # If it's not your turn, can't attack. - self.caller.msg("You can only do that on your turn.") - return - - if not self.caller.db.hp: # Can't attack if you have no HP. - self.caller.msg("You can't attack, you've been defeated.") - return - - attacker = self.caller - defender = self.caller.search(self.args) - - if not defender: # No valid target given. - return - - if not defender.db.hp: # Target object has no HP left or to begin with - self.caller.msg("You can't fight that!") - return - - if attacker == defender: # Target and attacker are the same - self.caller.msg("You can't attack yourself!") - return - - "If everything checks out, call the attack resolving function." - resolve_attack(attacker, defender) - spend_action(self.caller, 1, action_name="attack") # Use up one action. - - -class CmdPass(Command): - """ - Passes on your turn. - - Usage: - pass - - When in a fight, you can use this command to end your turn early, even - if there are still any actions you can take. - """ - - key = "pass" - aliases = ["wait", "hold"] - help_category = "combat" - - def func(self): - """ - This performs the actual command. - """ - if not is_in_combat(self.caller): # Can only pass a turn in combat. - self.caller.msg("You can only do that in combat. (see: help fight)") - return - - if not is_turn(self.caller): # Can only pass if it's your turn. - self.caller.msg("You can only do that on your turn.") - return - - self.caller.location.msg_contents("%s takes no further action, passing the turn." % self.caller) - spend_action(self.caller, 'all', action_name="pass") # Spend all remaining actions. - - -class CmdDisengage(Command): - """ - Passes your turn and attempts to end combat. - - Usage: - disengage - - Ends your turn early and signals that you're trying to end - the fight. If all participants in a fight disengage, the - fight ends. - """ - - key = "disengage" - aliases = ["spare"] - help_category = "combat" - - def func(self): - """ - This performs the actual command. - """ - if not is_in_combat(self.caller): # If you're not in combat - self.caller.msg("You can only do that in combat. (see: help fight)") - return - - if not is_turn(self.caller): # If it's not your turn - self.caller.msg("You can only do that on your turn.") - return - - self.caller.location.msg_contents("%s disengages, ready to stop fighting." % self.caller) - spend_action(self.caller, 'all', action_name="disengage") # Spend all remaining actions. - """ - The action_name kwarg sets the character's last action to "disengage", which is checked by - the turn handler script to see if all fighters have disengaged. - """ - - -class CmdRest(Command): - """ - Recovers damage. - - Usage: - rest - - Resting recovers your HP to its maximum, but you can only - rest if you're not in a fight. - """ - - key = "rest" - help_category = "combat" - - def func(self): - "This performs the actual command." - - if is_in_combat(self.caller): # If you're in combat - self.caller.msg("You can't rest while you're in combat.") - return - - self.caller.db.hp = self.caller.db.max_hp # Set current HP to maximum - self.caller.location.msg_contents("%s rests to recover HP." % self.caller) - """ - You'll probably want to replace this with your own system for recovering HP. - """ - - -class CmdCombatHelp(CmdHelp): - """ - View help or a list of topics - - Usage: - help - help list - help all - - This will search for help on commands and other - topics related to the game. - """ - # Just like the default help command, but will give quick - # tips on combat when used in a fight with no arguments. - - def func(self): - if is_in_combat(self.caller) and not self.args: # In combat and entered 'help' alone - self.caller.msg("Available combat commands:|/" + - "|wAttack:|n Attack a target, attempting to deal damage.|/" + - "|wPass:|n Pass your turn without further action.|/" + - "|wDisengage:|n End your turn and attempt to end combat.|/") - else: - super(CmdCombatHelp, self).func() # Call the default help command - - -class BattleCmdSet(default_cmds.CharacterCmdSet): - """ - This command set includes all the commmands used in the battle system. - """ - key = "DefaultCharacter" - - def at_cmdset_creation(self): - """ - Populates the cmdset - """ - self.add(CmdFight()) - self.add(CmdAttack()) - self.add(CmdRest()) - self.add(CmdPass()) - self.add(CmdDisengage()) - self.add(CmdCombatHelp()) - - -""" ----------------------------------------------------------------------------- -SCRIPTS START HERE ----------------------------------------------------------------------------- -""" - - -class TurnHandler(DefaultScript): - """ - This is the script that handles the progression of combat through turns. - On creation (when a fight is started) it adds all combat-ready characters - to its roster and then sorts them into a turn order. There can only be one - fight going on in a single room at a time, so the script is assigned to a - room as its object. - - Fights persist until only one participant is left with any HP or all - remaining participants choose to end the combat with the 'disengage' command. - """ - - def at_script_creation(self): - """ - Called once, when the script is created. - """ - self.key = "Combat Turn Handler" - self.interval = 5 # Once every 5 seconds - self.persistent = True - self.db.fighters = [] - - # Add all fighters in the room with at least 1 HP to the combat." - for object in self.obj.contents: - if object.db.hp: - self.db.fighters.append(object) - - # Initialize each fighter for combat - for fighter in self.db.fighters: - self.initialize_for_combat(fighter) - - # Add a reference to this script to the room - self.obj.db.Combat_TurnHandler = self - - # Roll initiative and sort the list of fighters depending on who rolls highest to determine turn order. - # The initiative roll is determined by the roll_init function and can be customized easily. - ordered_by_roll = sorted(self.db.fighters, key=roll_init, reverse=True) - self.db.fighters = ordered_by_roll - - # Announce the turn order. - self.obj.msg_contents("Turn order is: %s " % ", ".join(obj.key for obj in self.db.fighters)) - - # Set up the current turn and turn timeout delay. - self.db.turn = 0 - self.db.timer = 30 # 30 seconds - - def at_stop(self): - """ - Called at script termination. - """ - for fighter in self.db.fighters: - combat_cleanup(fighter) # Clean up the combat attributes for every fighter. - self.obj.db.Combat_TurnHandler = None # Remove reference to turn handler in location - - def at_repeat(self): - """ - Called once every self.interval seconds. - """ - currentchar = self.db.fighters[self.db.turn] # Note the current character in the turn order. - self.db.timer -= self.interval # Count down the timer. - - if self.db.timer <= 0: - # Force current character to disengage if timer runs out. - self.obj.msg_contents("%s's turn timed out!" % currentchar) - spend_action(currentchar, 'all', action_name="disengage") # Spend all remaining actions. - return - elif self.db.timer <= 10 and not self.db.timeout_warning_given: # 10 seconds left - # Warn the current character if they're about to time out. - currentchar.msg("WARNING: About to time out!") - self.db.timeout_warning_given = True - - def initialize_for_combat(self, character): - """ - Prepares a character for combat when starting or entering a fight. - - Args: - character (obj): Character to initialize for combat. - """ - combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case. - character.db.Combat_ActionsLeft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0 - character.db.Combat_TurnHandler = self # Add a reference to this turn handler script to the character - character.db.Combat_LastAction = "null" # Track last action taken in combat - - def start_turn(self, character): - """ - Readies a character for the start of their turn by replenishing their - available actions and notifying them that their turn has come up. - - Args: - character (obj): Character to be readied. - - Notes: - Here, you only get one action per turn, but you might want to allow more than - one per turn, or even grant a number of actions based on a character's - attributes. You can even add multiple different kinds of actions, I.E. actions - separated for movement, by adding "character.db.Combat_MovesLeft = 3" or - something similar. - """ - character.db.Combat_ActionsLeft = 1 # 1 action per turn. - # Prompt the character for their turn and give some information. - character.msg("|wIt's your turn! You have %i HP remaining.|n" % character.db.hp) - - def next_turn(self): - """ - Advances to the next character in the turn order. - """ - - # Check to see if every character disengaged as their last action. If so, end combat. - disengage_check = True - for fighter in self.db.fighters: - if fighter.db.Combat_LastAction != "disengage": # If a character has done anything but disengage - disengage_check = False - if disengage_check: # All characters have disengaged - self.obj.msg_contents("All fighters have disengaged! Combat is over!") - self.stop() # Stop this script and end combat. - return - - # Check to see if only one character is left standing. If so, end combat. - defeated_characters = 0 - for fighter in self.db.fighters: - if fighter.db.HP == 0: - defeated_characters += 1 # Add 1 for every fighter with 0 HP left (defeated) - if defeated_characters == (len(self.db.fighters) - 1): # If only one character isn't defeated - for fighter in self.db.fighters: - if fighter.db.HP != 0: - LastStanding = fighter # Pick the one fighter left with HP remaining - self.obj.msg_contents("Only %s remains! Combat is over!" % LastStanding) - self.stop() # Stop this script and end combat. - return - - # Cycle to the next turn. - currentchar = self.db.fighters[self.db.turn] - self.db.turn += 1 # Go to the next in the turn order. - if self.db.turn > len(self.db.fighters) - 1: - self.db.turn = 0 # Go back to the first in the turn order once you reach the end. - newchar = self.db.fighters[self.db.turn] # Note the new character - self.db.timer = 30 + self.time_until_next_repeat() # Reset the timer. - self.db.timeout_warning_given = False # Reset the timeout warning. - self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar)) - self.start_turn(newchar) # Start the new character's turn. - - def turn_end_check(self, character): - """ - Tests to see if a character's turn is over, and cycles to the next turn if it is. - - Args: - character (obj): Character to test for end of turn - """ - if not character.db.Combat_ActionsLeft: # Character has no actions remaining - self.next_turn() - return - - def join_fight(self, character): - """ - Adds a new character to a fight already in progress. - - Args: - character (obj): Character to be added to the fight. - """ - # Inserts the fighter to the turn order, right behind whoever's turn it currently is. - self.db.fighters.insert(self.db.turn, character) - # Tick the turn counter forward one to compensate. - self.db.turn += 1 - # Initialize the character like you do at the start. - self.initialize_for_combat(character) From d150d191e9b2a490e9d3124e21d4d7ccf3c55790 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sat, 14 Oct 2017 22:34:18 -0700 Subject: [PATCH 20/68] Add tb_range - movement & positioning for 'turnbattle' Adds a system for range and movement to the 'turnbattle' contrib. This is based on the abstract movement and positioning system I made for 'The World of Cool Battles', my learning project - fighters' absolute positions are not tracked, only their relative distance to each other and other objects. Commands for movement as well as distinction between melee and ranged attacks are included. --- evennia/contrib/turnbattle/tb_range.py | 1356 ++++++++++++++++++++++++ 1 file changed, 1356 insertions(+) create mode 100644 evennia/contrib/turnbattle/tb_range.py diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py new file mode 100644 index 0000000000..3061d8c873 --- /dev/null +++ b/evennia/contrib/turnbattle/tb_range.py @@ -0,0 +1,1356 @@ +""" +Simple turn-based combat system with range and movement + +Contrib - Tim Ashley Jenkins 2017 + +This is a version of the 'turnbattle' contrib that includes a system +for abstract movement and positioning in combat, including distinction +between melee and ranged attacks. In this system, a fighter or object's +exact position is not recorded - only their relative distance to other +actors in combat. + +In this example, the distance between two objects in combat is expressed +as an integer value: 0 for "engaged" objects that are right next to each +other, 1 for "reach" which is for objects that are near each other but +not directly adjacent, and 2 for "range" for objects that are far apart. + +When combat starts, all fighters are at reach with each other and other +objects, and at range from any exits. On a fighter's turn, they can use +the "approach" command to move closer to an object, or the "withdraw" +command to move further away from an object, either of which takes an +action in combat. In this example, fighters are given two actions per +turn, allowing them to move and attack in the same round, or to attack +twice or move twice. + +When you move toward an object, you will also move toward anything else +that's close to your target - the same goes for moving away from a target, +which will also move you away from anything close to your target. Moving +toward one target may also move you away from anything you're already +close to, but withdrawing from a target will never inadvertently bring +you closer to anything else. + +In this example, there are two attack commands. 'Attack' can only hit +targets that are 'engaged' (range 0) with you. 'Shoot' can hit any target +on the field, but cannot be used if you are engaged with any other fighters. +In addition, strikes made with the 'attack' command are more accurate than +'shoot' attacks. This is only to provide an example of how melee and ranged +attacks can be made to work differently - you can, of course, modify this +to fit your rules system. + +When in combat, the ranges of objects are also accounted for - you can't +pick up an object unless you're engaged with it, and can't give an object +to another fighter without being engaged with them either. Dropped objects +are automatically assigned a range of 'engaged' with the fighter who dropped +them. Additionally, giving or getting an object will take an action in combat. +Dropping an object does not take an action, but can only be done on your turn. + +When combat ends, all range values are erased and all restrictions on getting +or getting objects are lifted - distances are no longer tracked and objects in +the same room can be considered to be in the same space, as is the default +behavior of Evennia and most MUDs. + +This system allows for strategies in combat involving movement and +positioning to be implemented in your battle system without the use of +a 'grid' of coordinates, which can be difficult and clunky to navigate +in text and disadvantageous to players who use screen readers. This loose, +narrative method of tracking position is based around how the matter is +handled in tabletop RPGs played without a grid - typically, a character's +exact position in a room isn't important, only their relative distance to +other actors. + +You may wish to expand this system with a method of distinguishing allies +from enemies (to prevent allied characters from blocking your ranged attacks) +as well as some method by which melee-focused characters can prevent enemies +from withdrawing or punish them from doing so, such as by granting "attacks of +opportunity" or something similar. If you wish, you can also expand the breadth +of values allowed for range - rather than just 0, 1, and 2, you can allow ranges +to go up to much higher values, and give attacks and movements more varying +values for distance for a more granular system. You may also want to implement +a system for fleeing or changing rooms in combat by approaching exits, which +are objects placed in the range field like any other. + +To install and test, import this module's TBRangeCharacter object into +your game's character.py module: + + from evennia.contrib.turnbattle.tb_range import TBRangeCharacter + +And change your game's character typeclass to inherit from TBRangeCharacter +instead of the default: + + class Character(TBRangeCharacter): + +Next, import this module into your default_cmdsets.py module: + + from evennia.contrib.turnbattle import tb_range + +And add the battle command set to your default command set: + + # + # any commands you add below will overload the default ones. + # + self.add(tb_range.BattleCmdSet()) + +This module is meant to be heavily expanded on, so you may want to copy it +to your game's 'world' folder and modify it there rather than importing it +in your game and using it as-is. +""" + +from random import randint +from evennia import DefaultCharacter, DefaultObject, Command, default_cmds, DefaultScript +from evennia.commands.default.help import CmdHelp + +""" +---------------------------------------------------------------------------- +COMBAT FUNCTIONS START HERE +---------------------------------------------------------------------------- +""" + + +def roll_init(character): + """ + Rolls a number between 1-1000 to determine initiative. + + Args: + character (obj): The character to determine initiative for + + Returns: + initiative (int): The character's place in initiative - higher + numbers go first. + + Notes: + By default, does not reference the character and simply returns + a random integer from 1 to 1000. + + Since the character is passed to this function, you can easily reference + a character's stats to determine an initiative roll - for example, if your + character has a 'dexterity' attribute, you can use it to give that character + an advantage in turn order, like so: + + return (randint(1,20)) + character.db.dexterity + + This way, characters with a higher dexterity will go first more often. + """ + return randint(1, 1000) + + +def get_attack(attacker, defender, attack_type): + """ + Returns a value for an attack roll. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being attacked + attack_type (str): Type of attack ('melee' or 'ranged') + + Returns: + attack_value (int): Attack roll value, compared against a defense value + to determine whether an attack hits or misses. + + Notes: + By default, generates a random integer from 1 to 100 without using any + properties from either the attacker or defender, and modifies the result + based on whether it's for a melee or ranged attack. + + This can easily be expanded to return a value based on characters stats, + equipment, and abilities. This is why the attacker and defender are passed + to this function, even though nothing from either one are used in this example. + """ + # For this example, just return a random integer up to 100. + attack_value = randint(1, 100) + # Make melee attacks more accurate, ranged attacks less accurate + if attack_type == "melee": + attack_value += 15 + if attack_type == "ranged": + attack_value -= 15 + return attack_value + + +def get_defense(attacker, defender, attack_type): + """ + Returns a value for defense, which an attack roll must equal or exceed in order + for an attack to hit. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being attacked + attack_type (str): Type of attack ('melee' or 'ranged') + + Returns: + defense_value (int): Defense value, compared against an attack roll + to determine whether an attack hits or misses. + + Notes: + By default, returns 50, not taking any properties of the defender or + attacker into account. + + As above, this can be expanded upon based on character stats and equipment. + """ + # For this example, just return 50, for about a 50/50 chance of hit. + defense_value = 50 + return defense_value + + +def get_damage(attacker, defender): + """ + Returns a value for damage to be deducted from the defender's HP after abilities + successful hit. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being damaged + + Returns: + damage_value (int): Damage value, which is to be deducted from the defending + character's HP. + + Notes: + By default, returns a random integer from 15 to 25 without using any + properties from either the attacker or defender. + + Again, this can be expanded upon. + """ + # For this example, just generate a number between 15 and 25. + damage_value = randint(15, 25) + return damage_value + + +def apply_damage(defender, damage): + """ + Applies damage to a target, reducing their HP by the damage amount to a + minimum of 0. + + Args: + defender (obj): Character taking damage + damage (int): Amount of damage being taken + """ + defender.db.hp -= damage # Reduce defender's HP by the damage dealt. + # If this reduces it to 0 or less, set HP to 0. + if defender.db.hp <= 0: + defender.db.hp = 0 + + +def resolve_attack(attacker, defender, attack_type, attack_value=None, defense_value=None): + """ + Resolves an attack and outputs the result. + + Args: + attacker (obj): Character doing the attacking + defender (obj): Character being attacked + attack_type (str): Type of attack (melee or ranged) + + Notes: + Even though the attack and defense values are calculated + extremely simply, they are separated out into their own functions + so that they are easier to expand upon. + """ + # Get an attack roll from the attacker. + if not attack_value: + attack_value = get_attack(attacker, defender, attack_type) + # Get a defense value from the defender. + if not defense_value: + defense_value = get_defense(attacker, defender, attack_type) + # If the attack value is lower than the defense value, miss. Otherwise, hit. + if attack_value < defense_value: + attacker.location.msg_contents("%s's %s attack misses %s!" % (attacker, attack_type, defender)) + else: + damage_value = get_damage(attacker, defender) # Calculate damage value. + # Announce damage dealt and apply damage. + attacker.location.msg_contents("%s hits %s with a %s attack for %i damage!" % (attacker, defender, attack_type, damage_value)) + apply_damage(defender, damage_value) + # If defender HP is reduced to 0 or less, announce defeat. + if defender.db.hp <= 0: + attacker.location.msg_contents("%s has been defeated!" % defender) + +def distance_dec(mover, target): + """ + Decreases distance in range field between mover and target. + + Args: + mover (obj): The object moving + target (obj): The object to be moved toward + """ + mover.db.Combat_Range[target] -= 1 + target.db.Combat_Range[mover] = mover.db.Combat_Range[target] + # If this brings mover to range 0 (Engaged): + if mover.db.Combat_Range[target] <= 0: + # Reset range to each other to 0 and copy target's ranges to mover. + target.db.Combat_Range[mover] = 0 + mover.db.Combat_Range = target.db.Combat_Range + # Assure everything else has the same distance from the mover and target, now that they're together + for object in mover.location.contents: + if object != mover and object != target: + object.db.Combat_Range[mover] = object.db.Combat_Range[target] + +def distance_inc(mover, target): + """ + Increases distance in range field between mover and target. + + Args: + mover (obj): The object moving + target (obj): The object to be moved away from + """ + mover.db.Combat_Range[target] += 1 + target.db.Combat_Range[mover] = mover.db.Combat_Range[target] + # Set a cap of 2: + if mover.db.Combat_Range[target] > 2: + target.db.Combat_Range[mover] = 2 + mover.db.Combat_Range[target] = 2 + +def approach(mover, target): + """ + Manages a character's whole approach, including changes in ranges to other characters. + + Args: + mover (obj): The object moving + target (obj): The object to be moved toward + + Notes: + The mover will also automatically move toward any objects that are closer to the + target than the mover is. The mover will also move away from anything they started + out close to. + """ + objects = mover.location.contents + + for thing in objects: + if thing != mover and thing != target: + # Move closer to each object closer to the target than you. + if mover.db.Combat_Range[thing] > target.db.Combat_Range[thing]: + distance_dec(mover, thing) + # Move further from each object that's further from you than from the target. + if mover.db.Combat_Range[thing] < target.db.Combat_Range[thing]: + distance_inc(mover, thing) + # Lastly, move closer to your target. + distance_dec(mover, target) + return + +def withdraw(mover, target): + """ + Manages a character's whole withdrawal, including changes in ranges to other characters. + + Args: + mover (obj): The object moving + target (obj): The object to be moved away from + + Notes: + The mover will also automatically move away from objects that are close to the target + of their withdrawl. The mover will never inadvertently move toward anything else while + withdrawing - they can be considered to be moving to open space. + """ + objects = mover.location.contents + for thing in objects: + if thing != mover and thing != target: + # Move away from each object closer to the target than you, if it's also closer to you than you are to the target. + if mover.db.Combat_Range[thing] >= target.db.Combat_Range[thing] and mover.db.Combat_Range[thing] < mover.db.Combat_Range[thing]: + distance_inc(mover, thing) + # Move away from anything your target is engaged with + if target.db.Combat_Range[thing] == 0: + distance_inc(mover, thing) + # Move away from anything you're engaged with. + if mover.db.Combat_Range[thing] == 0: + distance_inc(mover, thing) + # Then, move away from your target. + distance_inc(mover, target) + return + +def get_range(obj1, obj2): + """ + Gets the combat range between two objects. + + Args: + obj1 (obj): First object + obj2 (obj): Second object + + Returns: + range (int or None): Distance between two objects or None if not applicable + """ + # Return None if not applicable. + if not obj1.db.Combat_Range: + return None + if not obj2.db.Combat_Range: + return None + if obj1 not in obj2.db.Combat_Range: + return None + if obj2 not in obj1.db.Combat_Range: + return None + # Return the range between the two objects. + return obj1.db.Combat_Range[obj2] + +def combat_cleanup(character): + """ + Cleans up all the temporary combat-related attributes on a character. + + Args: + character (obj): Character to have their combat attributes removed + + Notes: + Any attribute whose key begins with 'combat_' is temporary and no + longer needed once a fight ends. + """ + for attr in character.attributes.all(): + if attr.key[:7] == "combat_": # If the attribute name starts with 'combat_'... + character.attributes.remove(key=attr.key) # ...then delete it! + + +def is_in_combat(character): + """ + Returns true if the given character is in combat. + + Args: + character (obj): Character to determine if is in combat or not + + Returns: + (bool): True if in combat or False if not in combat + """ + if character.db.Combat_TurnHandler: + return True + return False + + +def is_turn(character): + """ + Returns true if it's currently the given character's turn in combat. + + Args: + character (obj): Character to determine if it is their turn or not + + Returns: + (bool): True if it is their turn or False otherwise + """ + turnhandler = character.db.Combat_TurnHandler + currentchar = turnhandler.db.fighters[turnhandler.db.turn] + if character == currentchar: + return True + return False + + +def spend_action(character, actions, action_name=None): + """ + Spends a character's available combat actions and checks for end of turn. + + Args: + character (obj): Character spending the action + actions (int) or 'all': Number of actions to spend, or 'all' to spend all actions + + Kwargs: + action_name (str or None): If a string is given, sets character's last action in + combat to provided string + """ + if action_name: + character.db.Combat_LastAction = action_name + if actions == 'all': # If spending all actions + character.db.Combat_ActionsLeft = 0 # Set actions to 0 + else: + character.db.Combat_ActionsLeft -= actions # Use up actions. + if character.db.Combat_ActionsLeft < 0: + character.db.Combat_ActionsLeft = 0 # Can't have fewer than 0 actions + character.db.Combat_TurnHandler.turn_end_check(character) # Signal potential end of turn. + +def combat_status_message(fighter): + """ + Sends a message to a player with their current HP and + distances to other fighters and objects. Called at turn + start and by the 'status' command. + """ + + status_msg = ("HP Remaining: %i / %i" % (fighter.db.hp, fighter.db.max_hp)) + + if not is_in_combat(fighter): + fighter.msg(status_msg) + return + + engaged_obj = [] + reach_obj = [] + range_obj = [] + + for object in fighter.db.Combat_Range: + if object != fighter: + if fighter.db.Combat_Range[object] == 0: + engaged_obj.append(object) + if fighter.db.Combat_Range[object] == 1: + reach_obj.append(object) + if fighter.db.Combat_Range[object] > 1: + range_obj.append(object) + + if engaged_obj: + status_msg += "|/Engaged targets: %s" % ", ".join(obj.key for obj in engaged_obj) + if reach_obj: + status_msg += "|/Reach targets: %s" % ", ".join(obj.key for obj in reach_obj) + if range_obj: + status_msg += "|/Ranged targets: %s" % ", ".join(obj.key for obj in range_obj) + + fighter.msg(status_msg) + return + + + +""" +---------------------------------------------------------------------------- +TYPECLASSES START HERE +---------------------------------------------------------------------------- +""" + + +class TBRangeCharacter(DefaultCharacter): + """ + A character able to participate in turn-based combat. Has attributes for current + and maximum HP, and access to combat commands. + """ + + def at_object_creation(self): + """ + Called once, when this object is first created. This is the + normal hook to overload for most object types. + """ + self.db.max_hp = 100 # Set maximum HP to 100 + self.db.hp = self.db.max_hp # Set current HP to maximum + """ + Adds attributes for a character's current and maximum HP. + We're just going to set this value at '100' by default. + + You may want to expand this to include various 'stats' that + can be changed at creation and factor into combat calculations. + """ + + def at_before_move(self, destination): + """ + Called just before starting to move this object to + destination. + + Args: + destination (Object): The object we are moving to + + Returns: + shouldmove (bool): If we should move or not. + + Notes: + If this method returns False/None, the move is cancelled + before it is even started. + + """ + # Keep the character from moving if at 0 HP or in combat. + if is_in_combat(self): + self.msg("You can't exit a room while in combat!") + return False # Returning false keeps the character from moving. + if self.db.HP <= 0: + self.msg("You can't move, you've been defeated!") + return False + return True + +class TBRangeObject(DefaultObject): + """ + An object that is assigned range values in combat. Getting, giving, and dropping + the object has restrictions in combat - you must be next to an object to get it, + must be next to your target to give them something, and can only interact with + objects on your own turn. + """ + def at_before_drop(self, dropper): + """ + Called by the default `drop` command before this object has been + dropped. + + Args: + dropper (Object): The object which will drop this object. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + + Returns: + shoulddrop (bool): If the object should be dropped or not. + + Notes: + If this method returns False/None, the dropping is cancelled + before it is even started. + + """ + # Can't drop something if in combat and it's not your turn + if is_in_combat(dropper) and not is_turn(dropper): + dropper.msg("You can only drop things on your turn!") + return False + return True + def at_drop(self, dropper): + """ + Called by the default `drop` command when this object has been + dropped. + + Args: + dropper (Object): The object which just dropped this object. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + + Notes: + This hook cannot stop the drop from happening. Use + permissions or the at_before_drop() hook for that. + + """ + # If dropper is currently in combat + if dropper.location.db.Combat_TurnHandler: + # Object joins the range field + self.db.Combat_Range = {} + dropper.location.db.Combat_TurnHandler.join_rangefield(self, anchor_obj=dropper) + def at_before_get(self, getter): + """ + Called by the default `get` command before this object has been + picked up. + + Args: + getter (Object): The object about to get this object. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + + Returns: + shouldget (bool): If the object should be gotten or not. + + Notes: + If this method returns False/None, the getting is cancelled + before it is even started. + """ + # Restrictions for getting in combat + if is_in_combat(getter): + if not is_turn(getter): # Not your turn + getter.msg("You can only get things on your turn!") + return False + if get_range(self, getter) > 0: # Too far away + getter.msg("You aren't close enough to get that! (see: help approach)") + return False + return True + def at_get(self, getter): + """ + Called by the default `get` command when this object has been + picked up. + + Args: + getter (Object): The object getting this object. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + + Notes: + This hook cannot stop the pickup from happening. Use + permissions or the at_before_get() hook for that. + + """ + # If gotten, erase range values + if self.db.Combat_Range: + del self.db.Combat_Range + # Remove this object from everyone's range fields + for object in getter.location.contents: + if object.db.Combat_Range: + if self in object.db.Combat_Range: + object.db.Combat_Range.pop(self, None) + # If in combat, getter spends an action + if is_in_combat(getter): + spend_action(getter, 1, action_name="get") # Use up one action. + def at_before_give(self, giver, getter): + """ + Called by the default `give` command before this object has been + given. + + Args: + giver (Object): The object about to give this object. + getter (Object): The object about to get this object. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + + Returns: + shouldgive (bool): If the object should be given or not. + + Notes: + If this method returns False/None, the giving is cancelled + before it is even started. + + """ + # Restrictions for giving in combat + if is_in_combat(giver): + if not is_turn(giver): # Not your turn + giver.msg("You can only give things on your turn!") + return False + if get_range(giver, getter) > 0: # Too far away from target + giver.msg("You aren't close enough to give things to %s! (see: help approach)" % getter) + return False + return True + def at_give(self, giver, getter): + """ + Called by the default `give` command when this object has been + given. + + Args: + giver (Object): The object giving this object. + getter (Object): The object getting this object. + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + + Notes: + This hook cannot stop the give from happening. Use + permissions or the at_before_give() hook for that. + + """ + # Spend an action if in combat + if is_in_combat(giver): + spend_action(giver, 1, action_name="give") # Use up one action. + +""" +---------------------------------------------------------------------------- +COMMANDS START HERE +---------------------------------------------------------------------------- +""" + + +class CmdFight(Command): + """ + Starts a fight with everyone in the same room as you. + + Usage: + fight + + When you start a fight, everyone in the room who is able to + fight is added to combat, and a turn order is randomly rolled. + When it's your turn, you can attack other characters. + """ + key = "fight" + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + here = self.caller.location + fighters = [] + + if not self.caller.db.hp: # If you don't have any hp + self.caller.msg("You can't start a fight if you've been defeated!") + return + if is_in_combat(self.caller): # Already in a fight + self.caller.msg("You're already in a fight!") + return + for thing in here.contents: # Test everything in the room to add it to the fight. + if thing.db.HP: # If the object has HP... + fighters.append(thing) # ...then add it to the fight. + if len(fighters) <= 1: # If you're the only able fighter in the room + self.caller.msg("There's nobody here to fight!") + return + if here.db.Combat_TurnHandler: # If there's already a fight going on... + here.msg_contents("%s joins the fight!" % self.caller) + here.db.Combat_TurnHandler.join_fight(self.caller) # Join the fight! + return + here.msg_contents("%s starts a fight!" % self.caller) + # Add a turn handler script to the room, which starts combat. + here.scripts.add("contrib.turnbattle.tb_range.TBRangeTurnHandler") + # Remember you'll have to change the path to the script if you copy this code to your own modules! + + +class CmdAttack(Command): + """ + Attacks another character in melee. + + Usage: + attack + + When in a fight, you may attack another character. The attack has + a chance to hit, and if successful, will deal damage. You can only + attack engaged targets - that is, targets that are right next to + you. Use the 'approach' command to get closer to a target. + """ + + key = "attack" + help_category = "combat" + + def func(self): + "This performs the actual command." + "Set the attacker to the caller and the defender to the target." + + if not is_in_combat(self.caller): # If not in combat, can't attack. + self.caller.msg("You can only do that in combat. (see: help fight)") + return + + if not is_turn(self.caller): # If it's not your turn, can't attack. + self.caller.msg("You can only do that on your turn.") + return + + if not self.caller.db.hp: # Can't attack if you have no HP. + self.caller.msg("You can't attack, you've been defeated.") + return + + attacker = self.caller + defender = self.caller.search(self.args) + + if not defender: # No valid target given. + return + + if not defender.db.hp: # Target object has no HP left or to begin with + self.caller.msg("You can't fight that!") + return + + if attacker == defender: # Target and attacker are the same + self.caller.msg("You can't attack yourself!") + return + + if not get_range(attacker, defender) == 0: # Target isn't in melee + self.caller.msg("%s is too far away to attack - you need to get closer! (see: help approach)" % defender) + return + + "If everything checks out, call the attack resolving function." + resolve_attack(attacker, defender, "melee") + spend_action(self.caller, 1, action_name="attack") # Use up one action. + +class CmdShoot(Command): + """ + Attacks another character from range. + + Usage: + shoot + + When in a fight, you may shoot another character. The attack has + a chance to hit, and if successful, will deal damage. You can attack + any target in combat by shooting, but can't shoot if there are any + targets engaged with you. Use the 'withdraw' command to retreat from + nearby enemies. + """ + + key = "shoot" + help_category = "combat" + + def func(self): + "This performs the actual command." + "Set the attacker to the caller and the defender to the target." + + if not is_in_combat(self.caller): # If not in combat, can't attack. + self.caller.msg("You can only do that in combat. (see: help fight)") + return + + if not is_turn(self.caller): # If it's not your turn, can't attack. + self.caller.msg("You can only do that on your turn.") + return + + if not self.caller.db.hp: # Can't attack if you have no HP. + self.caller.msg("You can't attack, you've been defeated.") + return + + attacker = self.caller + defender = self.caller.search(self.args) + + if not defender: # No valid target given. + return + + if not defender.db.hp: # Target object has no HP left or to begin with + self.caller.msg("You can't fight that!") + return + + if attacker == defender: # Target and attacker are the same + self.caller.msg("You can't attack yourself!") + return + + # Test to see if there are any nearby enemy targets. + in_melee = [] + for target in attacker.db.Combat_Range: + # Object is engaged and has HP + if get_range(attacker, defender) == 0 and target.db.hp and target != self.caller: + in_melee.append(target) # Add to list of targets in melee + + if len(in_melee) > 0: + self.caller.msg("You can't shoot because there are fighters engaged with you (%s) - you need to retreat! (see: help withdraw)" % ", ".join(obj.key for obj in in_melee)) + return + + "If everything checks out, call the attack resolving function." + resolve_attack(attacker, defender, "ranged") + spend_action(self.caller, 1, action_name="attack") # Use up one action. + +class CmdApproach(Command): + """ + Approaches an object. + + Usage: + approach + + Move one space toward a character or object. You can only attack + characters you are 0 spaces away from. + """ + + key = "approach" + help_category = "combat" + + def func(self): + "This performs the actual command." + + if not is_in_combat(self.caller): # If not in combat, can't approach. + self.caller.msg("You can only do that in combat. (see: help fight)") + return + + if not is_turn(self.caller): # If it's not your turn, can't approach. + self.caller.msg("You can only do that on your turn.") + return + + if not self.caller.db.hp: # Can't approach if you have no HP. + self.caller.msg("You can't move, you've been defeated.") + return + + mover = self.caller + target = self.caller.search(self.args) + + if not target: # No valid target given. + return + + if not target.db.Combat_Range: # Target object is not on the range field + self.caller.msg("You can't move toward that!") + return + + if mover == target: # Target and mover are the same + self.caller.msg("You can't move toward yourself!") + return + + if get_range(mover, target) <= 0: # Already engaged with target + self.caller.msg("You're already next to that target!") + return + + # If everything checks out, call the approach resolving function. + approach(mover, target) + mover.location.msg_contents("%s moves toward %s." % (mover, target)) + spend_action(self.caller, 1, action_name="move") # Use up one action. + +class CmdWithdraw(Command): + """ + Moves away from an object. + + Usage: + withdraw + + Move one space away from a character or object. + """ + + key = "withdraw" + help_category = "combat" + + def func(self): + "This performs the actual command." + + if not is_in_combat(self.caller): # If not in combat, can't withdraw. + self.caller.msg("You can only do that in combat. (see: help fight)") + return + + if not is_turn(self.caller): # If it's not your turn, can't withdraw. + self.caller.msg("You can only do that on your turn.") + return + + if not self.caller.db.hp: # Can't withdraw if you have no HP. + self.caller.msg("You can't move, you've been defeated.") + return + + mover = self.caller + target = self.caller.search(self.args) + + if not target: # No valid target given. + return + + if not target.db.Combat_Range: # Target object is not on the range field + self.caller.msg("You can't move away from that!") + return + + if mover == target: # Target and mover are the same + self.caller.msg("You can't move away from yourself!") + return + + if mover.db.Combat_Range[target] >= 3: # Already at maximum distance + self.caller.msg("You're as far as you can get from that target!") + return + + # If everything checks out, call the approach resolving function. + withdraw(mover, target) + mover.location.msg_contents("%s moves away from %s." % (mover, target)) + spend_action(self.caller, 1, action_name="move") # Use up one action. + + +class CmdPass(Command): + """ + Passes on your turn. + + Usage: + pass + + When in a fight, you can use this command to end your turn early, even + if there are still any actions you can take. + """ + + key = "pass" + aliases = ["wait", "hold"] + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + if not is_in_combat(self.caller): # Can only pass a turn in combat. + self.caller.msg("You can only do that in combat. (see: help fight)") + return + + if not is_turn(self.caller): # Can only pass if it's your turn. + self.caller.msg("You can only do that on your turn.") + return + + self.caller.location.msg_contents("%s takes no further action, passing the turn." % self.caller) + spend_action(self.caller, 'all', action_name="pass") # Spend all remaining actions. + + +class CmdDisengage(Command): + """ + Passes your turn and attempts to end combat. + + Usage: + disengage + + Ends your turn early and signals that you're trying to end + the fight. If all participants in a fight disengage, the + fight ends. + """ + + key = "disengage" + aliases = ["spare"] + help_category = "combat" + + def func(self): + """ + This performs the actual command. + """ + if not is_in_combat(self.caller): # If you're not in combat + self.caller.msg("You can only do that in combat. (see: help fight)") + return + + if not is_turn(self.caller): # If it's not your turn + self.caller.msg("You can only do that on your turn.") + return + + self.caller.location.msg_contents("%s disengages, ready to stop fighting." % self.caller) + spend_action(self.caller, 'all', action_name="disengage") # Spend all remaining actions. + """ + The action_name kwarg sets the character's last action to "disengage", which is checked by + the turn handler script to see if all fighters have disengaged. + """ + + +class CmdRest(Command): + """ + Recovers damage. + + Usage: + rest + + Resting recovers your HP to its maximum, but you can only + rest if you're not in a fight. + """ + + key = "rest" + help_category = "combat" + + def func(self): + "This performs the actual command." + + if is_in_combat(self.caller): # If you're in combat + self.caller.msg("You can't rest while you're in combat.") + return + + self.caller.db.hp = self.caller.db.max_hp # Set current HP to maximum + self.caller.location.msg_contents("%s rests to recover HP." % self.caller) + """ + You'll probably want to replace this with your own system for recovering HP. + """ + +class CmdStatus(Command): + """ + Gives combat information. + + Usage: + status + + Shows your current and maximum HP and your distance from + other targets in combat. + """ + + key = "status" + help_category = "combat" + + def func(self): + "This performs the actual command." + + combat_status_message(self.caller) + + +class CmdCombatHelp(CmdHelp): + """ + View help or a list of topics + + Usage: + help + help list + help all + + This will search for help on commands and other + topics related to the game. + """ + # Just like the default help command, but will give quick + # tips on combat when used in a fight with no arguments. + + def func(self): + if is_in_combat(self.caller) and not self.args: # In combat and entered 'help' alone + self.caller.msg("Available combat commands:|/" + + "|wAttack:|n Attack an engaged target, attempting to deal damage.|/" + + "|wShoot:|n Attack from a distance, if not engaged with other fighters.|/" + + "|wApproach:|n Move one step cloer to a target.|/" + + "|wWithdraw:|n Move one step away from a target.|/" + + "|wPass:|n Pass your turn without further action.|/" + + "|wStatus:|n View current HP and ranges to other targets.|/" + + "|wDisengage:|n End your turn and attempt to end combat.|/") + else: + super(CmdCombatHelp, self).func() # Call the default help command + + +class BattleCmdSet(default_cmds.CharacterCmdSet): + """ + This command set includes all the commmands used in the battle system. + """ + key = "DefaultCharacter" + + def at_cmdset_creation(self): + """ + Populates the cmdset + """ + self.add(CmdFight()) + self.add(CmdAttack()) + self.add(CmdShoot()) + self.add(CmdRest()) + self.add(CmdPass()) + self.add(CmdDisengage()) + self.add(CmdApproach()) + self.add(CmdWithdraw()) + self.add(CmdStatus()) + self.add(CmdCombatHelp()) + + +""" +---------------------------------------------------------------------------- +SCRIPTS START HERE +---------------------------------------------------------------------------- +""" + + +class TBRangeTurnHandler(DefaultScript): + """ + This is the script that handles the progression of combat through turns. + On creation (when a fight is started) it adds all combat-ready characters + to its roster and then sorts them into a turn order. There can only be one + fight going on in a single room at a time, so the script is assigned to a + room as its object. + + Fights persist until only one participant is left with any HP or all + remaining participants choose to end the combat with the 'disengage' command. + """ + + def at_script_creation(self): + """ + Called once, when the script is created. + """ + self.key = "Combat Turn Handler" + self.interval = 5 # Once every 5 seconds + self.persistent = True + self.db.fighters = [] + + # Add all fighters in the room with at least 1 HP to the combat." + for object in self.obj.contents: + if object.db.hp: + self.db.fighters.append(object) + + # Initialize each fighter for combat + for fighter in self.db.fighters: + self.initialize_for_combat(fighter) + + # Add a reference to this script to the room + self.obj.db.Combat_TurnHandler = self + + # Initialize range field for all objects in the room + for object in self.obj.contents: + self.init_range(object) + + # Roll initiative and sort the list of fighters depending on who rolls highest to determine turn order. + # The initiative roll is determined by the roll_init function and can be customized easily. + ordered_by_roll = sorted(self.db.fighters, key=roll_init, reverse=True) + self.db.fighters = ordered_by_roll + + # Announce the turn order. + self.obj.msg_contents("Turn order is: %s " % ", ".join(obj.key for obj in self.db.fighters)) + + # Start first fighter's turn. + self.start_turn(self.db.fighters[0]) + + # Set up the current turn and turn timeout delay. + self.db.turn = 0 + self.db.timer = 30 # 30 seconds + + def at_stop(self): + """ + Called at script termination. + """ + for object in self.obj.contents: + combat_cleanup(object) # Clean up the combat attributes for every object in the room. + self.obj.db.Combat_TurnHandler = None # Remove reference to turn handler in location + + def at_repeat(self): + """ + Called once every self.interval seconds. + """ + currentchar = self.db.fighters[self.db.turn] # Note the current character in the turn order. + self.db.timer -= self.interval # Count down the timer. + + if self.db.timer <= 0: + # Force current character to disengage if timer runs out. + self.obj.msg_contents("%s's turn timed out!" % currentchar) + spend_action(currentchar, 'all', action_name="disengage") # Spend all remaining actions. + return + elif self.db.timer <= 10 and not self.db.timeout_warning_given: # 10 seconds left + # Warn the current character if they're about to time out. + currentchar.msg("WARNING: About to time out!") + self.db.timeout_warning_given = True + + def init_range(self, to_init): + """ + Initializes range values for an object at the start of a fight. + + Args: + to_init (object): Object to initialize range field for. + """ + rangedict = {} + # Get a list of objects in the room. + objectlist = self.obj.contents + for object in objectlist: + # Object always at distance 0 from itself + if object == to_init: + rangedict.update({object:0}) + else: + if object.destination or to_init.destination: + # Start exits at range 2 to put them at the 'edges' + rangedict.update({object:2}) + else: + # Start objects at range 1 from other objects + rangedict.update({object:1}) + to_init.db.Combat_Range = rangedict + + def join_rangefield(self, to_init, anchor_obj=None, add_distance=0): + """ + Adds a new object to the range field of a fight in progress. + + Args: + to_init (object): Object to initialize range field for. + + Kwargs: + anchor_obj (object): Object to copy range values from, or None for a random object. + add_distance (int): Distance to put between to_init object and anchor object. + + """ + # Get a list of room's contents without to_init object. + contents = self.obj.contents + contents.remove(to_init) + # If no anchor object given, pick one in the room at random. + if not anchor_obj: + anchor_obj = contents[randint(0, (len(contents)-1))] + # Copy the range values from the anchor object. + to_init.db.Combat_Range = anchor_obj.db.Combat_Range + # Add the new object to everyone else's ranges. + for object in contents: + new_objects_range = object.db.Combat_Range[anchor_obj] + object.db.Combat_Range.update({to_init:new_objects_range}) + # Set the new object's range to itself to 0. + to_init.db.Combat_Range.update({to_init:0}) + # Add additional distance from anchor object, if any. + for n in range(add_distance): + withdraw(to_init, anchor_obj) + + def initialize_for_combat(self, character): + """ + Prepares a character for combat when starting or entering a fight. + + Args: + character (obj): Character to initialize for combat. + """ + combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case. + character.db.Combat_ActionsLeft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0 + character.db.Combat_TurnHandler = self # Add a reference to this turn handler script to the character + character.db.Combat_LastAction = "null" # Track last action taken in combat + + def start_turn(self, character): + """ + Readies a character for the start of their turn by replenishing their + available actions and notifying them that their turn has come up. + + Args: + character (obj): Character to be readied. + + Notes: + In this example, characters are given two actions per turn. This allows + characters to both move and attack in the same turn (or, alternately, + move twice or attack twice). + """ + character.db.Combat_ActionsLeft = 2 # 2 actions per turn. + # Prompt the character for their turn and give some information. + character.msg("|wIt's your turn!|n") + combat_status_message(character) + + def next_turn(self): + """ + Advances to the next character in the turn order. + """ + + # Check to see if every character disengaged as their last action. If so, end combat. + disengage_check = True + for fighter in self.db.fighters: + if fighter.db.Combat_LastAction != "disengage": # If a character has done anything but disengage + disengage_check = False + if disengage_check: # All characters have disengaged + self.obj.msg_contents("All fighters have disengaged! Combat is over!") + self.stop() # Stop this script and end combat. + return + + # Check to see if only one character is left standing. If so, end combat. + defeated_characters = 0 + for fighter in self.db.fighters: + if fighter.db.HP == 0: + defeated_characters += 1 # Add 1 for every fighter with 0 HP left (defeated) + if defeated_characters == (len(self.db.fighters) - 1): # If only one character isn't defeated + for fighter in self.db.fighters: + if fighter.db.HP != 0: + LastStanding = fighter # Pick the one fighter left with HP remaining + self.obj.msg_contents("Only %s remains! Combat is over!" % LastStanding) + self.stop() # Stop this script and end combat. + return + + # Cycle to the next turn. + currentchar = self.db.fighters[self.db.turn] + self.db.turn += 1 # Go to the next in the turn order. + if self.db.turn > len(self.db.fighters) - 1: + self.db.turn = 0 # Go back to the first in the turn order once you reach the end. + newchar = self.db.fighters[self.db.turn] # Note the new character + self.db.timer = 30 + self.time_until_next_repeat() # Reset the timer. + self.db.timeout_warning_given = False # Reset the timeout warning. + self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar)) + self.start_turn(newchar) # Start the new character's turn. + + def turn_end_check(self, character): + """ + Tests to see if a character's turn is over, and cycles to the next turn if it is. + + Args: + character (obj): Character to test for end of turn + """ + if not character.db.Combat_ActionsLeft: # Character has no actions remaining + self.next_turn() + return + + def join_fight(self, character): + """ + Adds a new character to a fight already in progress. + + Args: + character (obj): Character to be added to the fight. + """ + # Inserts the fighter to the turn order, right behind whoever's turn it currently is. + self.db.fighters.insert(self.db.turn, character) + # Tick the turn counter forward one to compensate. + self.db.turn += 1 + # Initialize the character like you do at the start. + self.initialize_for_combat(character) + # Add the character to the rangefield, at range from everyone, if they're not on it already. + if not character.db.Combat_Range: + self.join_rangefield(character, add_distance=2) From c762d3d7d173b200084cdd29692a47c43ef506d7 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sat, 14 Oct 2017 22:36:18 -0700 Subject: [PATCH 21/68] Update README.md Adds information about the tb_range module to the readme. --- evennia/contrib/turnbattle/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/evennia/contrib/turnbattle/README.md b/evennia/contrib/turnbattle/README.md index 8d709d1ba1..af04f80060 100644 --- a/evennia/contrib/turnbattle/README.md +++ b/evennia/contrib/turnbattle/README.md @@ -21,6 +21,11 @@ implemented and customized: the battle system, including commands for wielding weapons and donning armor, and modifiers to accuracy and damage based on currently used equipment. + + tb_range.py - Adds a system for abstract positioning and movement, which + tracks the distance between different characters and objects in + combat, as well as differentiates between melee and ranged + attacks. This system is meant as a basic framework to start from, and is modeled after the combat systems of popular tabletop role playing games rather than From fe6be5069a5a3aee9c9bfc36b250413080a4773a Mon Sep 17 00:00:00 2001 From: AmberFennek Date: Mon, 16 Oct 2017 15:13:53 -0400 Subject: [PATCH 22/68] Text corrections in comments and strings --- CHANGELOG.md | 2 +- evennia/accounts/accounts.py | 2 +- evennia/commands/default/account.py | 2 +- evennia/commands/default/unloggedin.py | 4 ++-- evennia/contrib/email_login.py | 2 +- evennia/utils/spawner.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cecd0de21..a05d65fdc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ # Sept 2017: Release of Evennia 0.7; upgrade to Django 1.11, change 'Player' to 'Account', rework the website template and a slew of other updates. -Info on what changed and how to migrat is found here: +Info on what changed and how to migrate is found here: https://groups.google.com/forum/#!msg/evennia/0JYYNGY-NfE/cDFaIwmPBAAJ ## Feb 2017: diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 43dfe71795..9feed46d2e 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -761,7 +761,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)): elif _MULTISESSION_MODE in (2, 3): # In this mode we by default end up at a character selection # screen. We execute look on the account. - # we make sure to clean up the _playable_characers list in case + # we make sure to clean up the _playable_characters list in case # any was deleted in the interim. self.db._playable_characters = [char for char in self.db._playable_characters if char] self.msg(self.at_look(target=self.db._playable_characters, diff --git a/evennia/commands/default/account.py b/evennia/commands/default/account.py index e45e2a9347..90c2fdb954 100644 --- a/evennia/commands/default/account.py +++ b/evennia/commands/default/account.py @@ -168,7 +168,7 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS): if desc: new_character.db.desc = desc elif not new_character.db.desc: - new_character.db.desc = "This is an Account." + new_character.db.desc = "This is a character." self.msg("Created new character %s. Use |w@ic %s|n to enter the game as this character." % (new_character.key, new_character.key)) diff --git a/evennia/commands/default/unloggedin.py b/evennia/commands/default/unloggedin.py index 429948b0fb..67e15dfd38 100644 --- a/evennia/commands/default/unloggedin.py +++ b/evennia/commands/default/unloggedin.py @@ -293,7 +293,7 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS): session.msg(string) return if not re.findall(r"^[\w. @+\-']+$", password) or not (3 < len(password)): - string = "\n\r Password should be longer than 3 characers. Letters, spaces, digits and @/./+/-/_/' only." \ + string = "\n\r Password should be longer than 3 characters. Letters, spaces, digits and @/./+/-/_/' only." \ "\nFor best security, make it longer than 8 characters. You can also use a phrase of" \ "\nmany words if you enclose the password in double quotes." session.msg(string) @@ -557,7 +557,7 @@ def _create_character(session, new_account, typeclass, home, permissions): # If no description is set, set a default description if not new_character.db.desc: - new_character.db.desc = "This is an Account." + new_character.db.desc = "This is a character." # We need to set this to have @ic auto-connect to this character new_account.db._last_puppet = new_character except Exception as e: diff --git a/evennia/contrib/email_login.py b/evennia/contrib/email_login.py index 01d08abab4..c98bf4aa88 100644 --- a/evennia/contrib/email_login.py +++ b/evennia/contrib/email_login.py @@ -197,7 +197,7 @@ class CmdUnconnectedCreate(MuxCommand): session.msg(string) return if not re.findall(r"^[\w. @+\-']+$", password) or not (3 < len(password)): - string = "\n\r Password should be longer than 3 characers. Letters, spaces, digits and @/./+/-/_/' only." \ + string = "\n\r Password should be longer than 3 characters. Letters, spaces, digits and @/./+/-/_/' only." \ "\nFor best security, make it longer than 8 characters. You can also use a phrase of" \ "\nmany words if you enclose the password in double quotes." session.msg(string) diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 4a8ac946c8..6df11985f1 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -174,7 +174,7 @@ def _batch_create_object(*objparams): objects (list): A list of created objects Notes: - The `exec` list will execute arbitrary python code so don't allow this to be availble to + The `exec` list will execute arbitrary python code so don't allow this to be available to unprivileged users! """ From 57f0abe37096e416062f2ff19b2fe013aa7547bb Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sun, 22 Oct 2017 10:50:36 -0400 Subject: [PATCH 23/68] Add notification support to a few older browsers and Safari --- evennia/web/webclient/static/webclient/js/webclient_gui.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/evennia/web/webclient/static/webclient/js/webclient_gui.js b/evennia/web/webclient/static/webclient/js/webclient_gui.js index ba657858d9..849993dd75 100644 --- a/evennia/web/webclient/static/webclient/js/webclient_gui.js +++ b/evennia/web/webclient/static/webclient/js/webclient_gui.js @@ -374,7 +374,10 @@ function onNewLine(text, originator) { document.title = "(" + unread + ") " + originalTitle; if ("Notification" in window){ if (("notification_popup" in options) && (options["notification_popup"])) { - Notification.requestPermission().then(function(result) { + // There is a Promise-based API for this, but it’s not supported + // in Safari and some older browsers: + // https://developer.mozilla.org/en-US/docs/Web/API/Notification/requestPermission#Browser_compatibility + Notification.requestPermission(function(result) { if(result === "granted") { var title = originalTitle === "" ? "Evennia" : originalTitle; var options = { From 5f516215eec6bfd29cbe4361f31003573c84ed08 Mon Sep 17 00:00:00 2001 From: Nicholas Matlaga Date: Sun, 22 Oct 2017 15:34:13 -0400 Subject: [PATCH 24/68] Change aliases of ExtendedRoom's desc command to reflect 0.7 hanges' --- evennia/contrib/extended_room.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/evennia/contrib/extended_room.py b/evennia/contrib/extended_room.py index 962578fa11..6823ede50e 100644 --- a/evennia/contrib/extended_room.py +++ b/evennia/contrib/extended_room.py @@ -4,7 +4,7 @@ Extended Room Evennia Contribution - Griatch 2012 This is an extended Room typeclass for Evennia. It is supported -by an extended `Look` command and an extended `@desc` command, also +by an extended `Look` command and an extended `desc` command, also in this module. @@ -21,7 +21,7 @@ There is also a general description which is used as fallback if one or more of the seasonal descriptions are not set when their time comes. -An updated `@desc` command allows for setting seasonal descriptions. +An updated `desc` command allows for setting seasonal descriptions. The room uses the `evennia.utils.gametime.GameTime` global script. This is started by default, but if you have deactivated it, you need to @@ -45,13 +45,13 @@ at, without there having to be a database object created for it. The Details are simply stored in a dictionary on the room and if the look command cannot find an object match for a `look ` command it will also look through the available details at the current location -if applicable. An extended `@desc` command is used to set details. +if applicable. An extended `desc` command is used to set details. 4) Extra commands CmdExtendedLook - look command supporting room details - CmdExtendedDesc - @desc command allowing to add seasonal descs and details, + CmdExtendedDesc - desc command allowing to add seasonal descs and details, as well as listing them CmdGameTime - A simple `time` command, displaying the current time and season. @@ -63,7 +63,7 @@ Installation/testing: (see Wiki for how to do this). 2) `@dig` a room of type `contrib.extended_room.ExtendedRoom` (or make it the default room type) -3) Use `@desc` and `@detail` to customize the room, then play around! +3) Use `desc` and `detail` to customize the room, then play around! """ from __future__ import division @@ -333,26 +333,26 @@ class CmdExtendedLook(default_cmds.CmdLook): class CmdExtendedDesc(default_cmds.CmdDesc): """ - `@desc` - describe an object or room. + `desc` - describe an object or room. Usage: - @desc[/switch] [ =] - @detail[/del] [ = ] + desc[/switch] [ =] + detail[/del] [ = ] - Switches for `@desc`: + Switches for `desc`: spring - set description for in current room. summer autumn winter - Switch for `@detail`: + Switch for `detail`: del - delete a named detail. Sets the "desc" attribute on an object. If an object is not given, describe the current room. - The alias `@detail` allows to assign a "detail" (a non-object + The alias `detail` allows to assign a "detail" (a non-object target for the `look` command) to the current room (only). You can also embed special time markers in your room description, like this: @@ -364,11 +364,11 @@ class CmdExtendedDesc(default_cmds.CmdDesc): Text marked this way will only display when the server is truly at the given timeslot. The available times are night, morning, afternoon and evening. - Note that `@detail`, seasons and time-of-day slots only work on rooms in this - version of the `@desc` command. + Note that `detail`, seasons and time-of-day slots only work on rooms in this + version of the `desc` command. """ - aliases = ["@describe", "@detail"] + aliases = ["describe", "detail"] def reset_times(self, obj): """By deleteting the caches we force a re-load.""" @@ -416,7 +416,7 @@ class CmdExtendedDesc(default_cmds.CmdDesc): self.reset_times(location) return else: - # we are doing a @desc call + # we are doing a desc call if not self.args: if location: string = "|wDescriptions on %s|n:\n" % location.key From f356b6b6d34e79ff438907a9aab3265b14f9e7fc Mon Sep 17 00:00:00 2001 From: bclack Date: Sat, 21 Oct 2017 14:46:18 -0500 Subject: [PATCH 25/68] Add newline after multimatch string error message Other utility functions, such as caller.msg(), don't require adding newline because newlines are added implicitly. This change modifies the multimatch string parameter to also not require newline, but add one implicitly. --- evennia/utils/utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 1a159991ba..667f6a209b 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -1786,8 +1786,12 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs): error = kwargs.get("nofound_string") or _("Could not find '%s'." % query) matches = None elif len(matches) > 1: - error = kwargs.get("multimatch_string") or \ - _("More than one match for '%s' (please narrow target):\n" % query) + multimatch_string = kwargs.get("multimatch_string") + if multimatch_string: + error = "%s\n" % multimatch_string + else: + error = _("More than one match for '%s' (please narrow target):\n" % query) + for num, result in enumerate(matches): # we need to consider Commands, where .aliases is a list aliases = result.aliases.all() if hasattr(result.aliases, "all") else result.aliases From 6e619d1949153e00175eff313451c63efeadb2c1 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 23 Oct 2017 21:02:42 -0700 Subject: [PATCH 26/68] Add at_defeat hook, PEP8 capitalization --- evennia/contrib/turnbattle/tb_basic.py | 57 ++++++---- evennia/contrib/turnbattle/tb_equip.py | 56 ++++++---- evennia/contrib/turnbattle/tb_range.py | 140 ++++++++++++++----------- 3 files changed, 147 insertions(+), 106 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_basic.py b/evennia/contrib/turnbattle/tb_basic.py index 70c81debae..22a96016c8 100644 --- a/evennia/contrib/turnbattle/tb_basic.py +++ b/evennia/contrib/turnbattle/tb_basic.py @@ -167,6 +167,20 @@ def apply_damage(defender, damage): if defender.db.hp <= 0: defender.db.hp = 0 +def at_defeat(defeated): + """ + Announces the defeat of a fighter in combat. + + Args: + defeated (obj): Fighter that's been defeated. + + Notes: + All this does is announce a defeat message by default, but if you + want anything else to happen to defeated fighters (like putting them + into a dying state or something similar) then this is the place to + do it. + """ + defeated.location.msg_contents("%s has been defeated!" % defeated) def resolve_attack(attacker, defender, attack_value=None, defense_value=None): """ @@ -195,10 +209,9 @@ def resolve_attack(attacker, defender, attack_value=None, defense_value=None): # Announce damage dealt and apply damage. attacker.location.msg_contents("%s hits %s for %i damage!" % (attacker, defender, damage_value)) apply_damage(defender, damage_value) - # If defender HP is reduced to 0 or less, announce defeat. + # If defender HP is reduced to 0 or less, call at_defeat. if defender.db.hp <= 0: - attacker.location.msg_contents("%s has been defeated!" % defender) - + at_defeat(defender) def combat_cleanup(character): """ @@ -226,7 +239,7 @@ def is_in_combat(character): Returns: (bool): True if in combat or False if not in combat """ - if character.db.Combat_TurnHandler: + if character.db.combat_turnhandler: return True return False @@ -241,7 +254,7 @@ def is_turn(character): Returns: (bool): True if it is their turn or False otherwise """ - turnhandler = character.db.Combat_TurnHandler + turnhandler = character.db.combat_turnhandler currentchar = turnhandler.db.fighters[turnhandler.db.turn] if character == currentchar: return True @@ -261,14 +274,14 @@ def spend_action(character, actions, action_name=None): combat to provided string """ if action_name: - character.db.Combat_LastAction = action_name + character.db.combat_lastaction = action_name if actions == 'all': # If spending all actions - character.db.Combat_ActionsLeft = 0 # Set actions to 0 + character.db.combat_actionsleft = 0 # Set actions to 0 else: - character.db.Combat_ActionsLeft -= actions # Use up actions. - if character.db.Combat_ActionsLeft < 0: - character.db.Combat_ActionsLeft = 0 # Can't have fewer than 0 actions - character.db.Combat_TurnHandler.turn_end_check(character) # Signal potential end of turn. + character.db.combat_actionsleft -= actions # Use up actions. + if character.db.combat_actionsleft < 0: + character.db.combat_actionsleft = 0 # Can't have fewer than 0 actions + character.db.combat_turnhandler.turn_end_check(character) # Signal potential end of turn. """ @@ -365,9 +378,9 @@ class CmdFight(Command): if len(fighters) <= 1: # If you're the only able fighter in the room self.caller.msg("There's nobody here to fight!") return - if here.db.Combat_TurnHandler: # If there's already a fight going on... + if here.db.combat_turnhandler: # If there's already a fight going on... here.msg_contents("%s joins the fight!" % self.caller) - here.db.Combat_TurnHandler.join_fight(self.caller) # Join the fight! + here.db.combat_turnhandler.join_fight(self.caller) # Join the fight! return here.msg_contents("%s starts a fight!" % self.caller) # Add a turn handler script to the room, which starts combat. @@ -600,7 +613,7 @@ class TBBasicTurnHandler(DefaultScript): self.initialize_for_combat(fighter) # Add a reference to this script to the room - self.obj.db.Combat_TurnHandler = self + self.obj.db.combat_turnhandler = self # Roll initiative and sort the list of fighters depending on who rolls highest to determine turn order. # The initiative roll is determined by the roll_init function and can be customized easily. @@ -623,7 +636,7 @@ class TBBasicTurnHandler(DefaultScript): """ for fighter in self.db.fighters: combat_cleanup(fighter) # Clean up the combat attributes for every fighter. - self.obj.db.Combat_TurnHandler = None # Remove reference to turn handler in location + self.obj.db.combat_turnhandler = None # Remove reference to turn handler in location def at_repeat(self): """ @@ -650,9 +663,9 @@ class TBBasicTurnHandler(DefaultScript): character (obj): Character to initialize for combat. """ combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case. - character.db.Combat_ActionsLeft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0 - character.db.Combat_TurnHandler = self # Add a reference to this turn handler script to the character - character.db.Combat_LastAction = "null" # Track last action taken in combat + character.db.combat_actionsleft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0 + character.db.combat_turnhandler = self # Add a reference to this turn handler script to the character + character.db.combat_lastaction = "null" # Track last action taken in combat def start_turn(self, character): """ @@ -666,10 +679,10 @@ class TBBasicTurnHandler(DefaultScript): Here, you only get one action per turn, but you might want to allow more than one per turn, or even grant a number of actions based on a character's attributes. You can even add multiple different kinds of actions, I.E. actions - separated for movement, by adding "character.db.Combat_MovesLeft = 3" or + separated for movement, by adding "character.db.combat_movesleft = 3" or something similar. """ - character.db.Combat_ActionsLeft = 1 # 1 action per turn. + character.db.combat_actionsleft = 1 # 1 action per turn. # Prompt the character for their turn and give some information. character.msg("|wIt's your turn! You have %i HP remaining.|n" % character.db.hp) @@ -681,7 +694,7 @@ class TBBasicTurnHandler(DefaultScript): # Check to see if every character disengaged as their last action. If so, end combat. disengage_check = True for fighter in self.db.fighters: - if fighter.db.Combat_LastAction != "disengage": # If a character has done anything but disengage + if fighter.db.combat_lastaction != "disengage": # If a character has done anything but disengage disengage_check = False if disengage_check: # All characters have disengaged self.obj.msg_contents("All fighters have disengaged! Combat is over!") @@ -719,7 +732,7 @@ class TBBasicTurnHandler(DefaultScript): Args: character (obj): Character to test for end of turn """ - if not character.db.Combat_ActionsLeft: # Character has no actions remaining + if not character.db.combat_actionsleft: # Character has no actions remaining self.next_turn() return diff --git a/evennia/contrib/turnbattle/tb_equip.py b/evennia/contrib/turnbattle/tb_equip.py index 7d9ea58442..b415f82428 100644 --- a/evennia/contrib/turnbattle/tb_equip.py +++ b/evennia/contrib/turnbattle/tb_equip.py @@ -203,6 +203,20 @@ def apply_damage(defender, damage): if defender.db.hp <= 0: defender.db.hp = 0 +def at_defeat(defeated): + """ + Announces the defeat of a fighter in combat. + + Args: + defeated (obj): Fighter that's been defeated. + + Notes: + All this does is announce a defeat message by default, but if you + want anything else to happen to defeated fighters (like putting them + into a dying state or something similar) then this is the place to + do it. + """ + defeated.location.msg_contents("%s has been defeated!" % defeated) def resolve_attack(attacker, defender, attack_value=None, defense_value=None): """ @@ -239,9 +253,9 @@ def resolve_attack(attacker, defender, attack_value=None, defense_value=None): else: attacker.location.msg_contents("%s's %s bounces harmlessly off %s!" % (attacker, attackers_weapon, defender)) apply_damage(defender, damage_value) - # If defender HP is reduced to 0 or less, announce defeat. + # If defender HP is reduced to 0 or less, call at_defeat. if defender.db.hp <= 0: - attacker.location.msg_contents("%s has been defeated!" % defender) + at_defeat(defender) def combat_cleanup(character): @@ -270,7 +284,7 @@ def is_in_combat(character): Returns: (bool): True if in combat or False if not in combat """ - if character.db.Combat_TurnHandler: + if character.db.combat_turnhandler: return True return False @@ -285,7 +299,7 @@ def is_turn(character): Returns: (bool): True if it is their turn or False otherwise """ - turnhandler = character.db.Combat_TurnHandler + turnhandler = character.db.combat_turnhandler currentchar = turnhandler.db.fighters[turnhandler.db.turn] if character == currentchar: return True @@ -305,14 +319,14 @@ def spend_action(character, actions, action_name=None): combat to provided string """ if action_name: - character.db.Combat_LastAction = action_name + character.db.combat_lastaction = action_name if actions == 'all': # If spending all actions - character.db.Combat_ActionsLeft = 0 # Set actions to 0 + character.db.combat_actionsleft = 0 # Set actions to 0 else: - character.db.Combat_ActionsLeft -= actions # Use up actions. - if character.db.Combat_ActionsLeft < 0: - character.db.Combat_ActionsLeft = 0 # Can't have fewer than 0 actions - character.db.Combat_TurnHandler.turn_end_check(character) # Signal potential end of turn. + character.db.combat_actionsleft -= actions # Use up actions. + if character.db.combat_actionsleft < 0: + character.db.combat_actionsleft = 0 # Can't have fewer than 0 actions + character.db.combat_turnhandler.turn_end_check(character) # Signal potential end of turn. """ @@ -482,9 +496,9 @@ class CmdFight(Command): if len(fighters) <= 1: # If you're the only able fighter in the room self.caller.msg("There's nobody here to fight!") return - if here.db.Combat_TurnHandler: # If there's already a fight going on... + if here.db.combat_turnhandler: # If there's already a fight going on... here.msg_contents("%s joins the fight!" % self.caller) - here.db.Combat_TurnHandler.join_fight(self.caller) # Join the fight! + here.db.combat_turnhandler.join_fight(self.caller) # Join the fight! return here.msg_contents("%s starts a fight!" % self.caller) # Add a turn handler script to the room, which starts combat. @@ -873,7 +887,7 @@ class TBEquipTurnHandler(DefaultScript): self.initialize_for_combat(fighter) # Add a reference to this script to the room - self.obj.db.Combat_TurnHandler = self + self.obj.db.combat_turnhandler = self # Roll initiative and sort the list of fighters depending on who rolls highest to determine turn order. # The initiative roll is determined by the roll_init function and can be customized easily. @@ -896,7 +910,7 @@ class TBEquipTurnHandler(DefaultScript): """ for fighter in self.db.fighters: combat_cleanup(fighter) # Clean up the combat attributes for every fighter. - self.obj.db.Combat_TurnHandler = None # Remove reference to turn handler in location + self.obj.db.combat_turnhandler = None # Remove reference to turn handler in location def at_repeat(self): """ @@ -923,9 +937,9 @@ class TBEquipTurnHandler(DefaultScript): character (obj): Character to initialize for combat. """ combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case. - character.db.Combat_ActionsLeft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0 - character.db.Combat_TurnHandler = self # Add a reference to this turn handler script to the character - character.db.Combat_LastAction = "null" # Track last action taken in combat + character.db.combat_actionsleft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0 + character.db.combat_turnhandler = self # Add a reference to this turn handler script to the character + character.db.combat_lastaction = "null" # Track last action taken in combat def start_turn(self, character): """ @@ -939,10 +953,10 @@ class TBEquipTurnHandler(DefaultScript): Here, you only get one action per turn, but you might want to allow more than one per turn, or even grant a number of actions based on a character's attributes. You can even add multiple different kinds of actions, I.E. actions - separated for movement, by adding "character.db.Combat_MovesLeft = 3" or + separated for movement, by adding "character.db.combat_movesleft = 3" or something similar. """ - character.db.Combat_ActionsLeft = 1 # 1 action per turn. + character.db.combat_actionsleft = 1 # 1 action per turn. # Prompt the character for their turn and give some information. character.msg("|wIt's your turn! You have %i HP remaining.|n" % character.db.hp) @@ -954,7 +968,7 @@ class TBEquipTurnHandler(DefaultScript): # Check to see if every character disengaged as their last action. If so, end combat. disengage_check = True for fighter in self.db.fighters: - if fighter.db.Combat_LastAction != "disengage": # If a character has done anything but disengage + if fighter.db.combat_lastaction != "disengage": # If a character has done anything but disengage disengage_check = False if disengage_check: # All characters have disengaged self.obj.msg_contents("All fighters have disengaged! Combat is over!") @@ -992,7 +1006,7 @@ class TBEquipTurnHandler(DefaultScript): Args: character (obj): Character to test for end of turn """ - if not character.db.Combat_ActionsLeft: # Character has no actions remaining + if not character.db.combat_actionsleft: # Character has no actions remaining self.next_turn() return diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index 3061d8c873..27566d4d27 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -228,6 +228,20 @@ def apply_damage(defender, damage): if defender.db.hp <= 0: defender.db.hp = 0 +def at_defeat(defeated): + """ + Announces the defeat of a fighter in combat. + + Args: + defeated (obj): Fighter that's been defeated. + + Notes: + All this does is announce a defeat message by default, but if you + want anything else to happen to defeated fighters (like putting them + into a dying state or something similar) then this is the place to + do it. + """ + defeated.location.msg_contents("%s has been defeated!" % defeated) def resolve_attack(attacker, defender, attack_type, attack_value=None, defense_value=None): """ @@ -257,9 +271,9 @@ def resolve_attack(attacker, defender, attack_type, attack_value=None, defense_v # Announce damage dealt and apply damage. attacker.location.msg_contents("%s hits %s with a %s attack for %i damage!" % (attacker, defender, attack_type, damage_value)) apply_damage(defender, damage_value) - # If defender HP is reduced to 0 or less, announce defeat. + # If defender HP is reduced to 0 or less, call at_defeat. if defender.db.hp <= 0: - attacker.location.msg_contents("%s has been defeated!" % defender) + at_defeat(defender) def distance_dec(mover, target): """ @@ -269,17 +283,17 @@ def distance_dec(mover, target): mover (obj): The object moving target (obj): The object to be moved toward """ - mover.db.Combat_Range[target] -= 1 - target.db.Combat_Range[mover] = mover.db.Combat_Range[target] + mover.db.combat_range[target] -= 1 + target.db.combat_range[mover] = mover.db.combat_range[target] # If this brings mover to range 0 (Engaged): - if mover.db.Combat_Range[target] <= 0: + if mover.db.combat_range[target] <= 0: # Reset range to each other to 0 and copy target's ranges to mover. - target.db.Combat_Range[mover] = 0 - mover.db.Combat_Range = target.db.Combat_Range + target.db.combat_range[mover] = 0 + mover.db.combat_range = target.db.combat_range # Assure everything else has the same distance from the mover and target, now that they're together for object in mover.location.contents: if object != mover and object != target: - object.db.Combat_Range[mover] = object.db.Combat_Range[target] + object.db.combat_range[mover] = object.db.combat_range[target] def distance_inc(mover, target): """ @@ -289,12 +303,12 @@ def distance_inc(mover, target): mover (obj): The object moving target (obj): The object to be moved away from """ - mover.db.Combat_Range[target] += 1 - target.db.Combat_Range[mover] = mover.db.Combat_Range[target] + mover.db.combat_range[target] += 1 + target.db.combat_range[mover] = mover.db.combat_range[target] # Set a cap of 2: - if mover.db.Combat_Range[target] > 2: - target.db.Combat_Range[mover] = 2 - mover.db.Combat_Range[target] = 2 + if mover.db.combat_range[target] > 2: + target.db.combat_range[mover] = 2 + mover.db.combat_range[target] = 2 def approach(mover, target): """ @@ -314,10 +328,10 @@ def approach(mover, target): for thing in objects: if thing != mover and thing != target: # Move closer to each object closer to the target than you. - if mover.db.Combat_Range[thing] > target.db.Combat_Range[thing]: + if mover.db.combat_range[thing] > target.db.combat_range[thing]: distance_dec(mover, thing) # Move further from each object that's further from you than from the target. - if mover.db.Combat_Range[thing] < target.db.Combat_Range[thing]: + if mover.db.combat_range[thing] < target.db.combat_range[thing]: distance_inc(mover, thing) # Lastly, move closer to your target. distance_dec(mover, target) @@ -340,13 +354,13 @@ def withdraw(mover, target): for thing in objects: if thing != mover and thing != target: # Move away from each object closer to the target than you, if it's also closer to you than you are to the target. - if mover.db.Combat_Range[thing] >= target.db.Combat_Range[thing] and mover.db.Combat_Range[thing] < mover.db.Combat_Range[thing]: + if mover.db.combat_range[thing] >= target.db.combat_range[thing] and mover.db.combat_range[thing] < mover.db.combat_range[thing]: distance_inc(mover, thing) # Move away from anything your target is engaged with - if target.db.Combat_Range[thing] == 0: + if target.db.combat_range[thing] == 0: distance_inc(mover, thing) # Move away from anything you're engaged with. - if mover.db.Combat_Range[thing] == 0: + if mover.db.combat_range[thing] == 0: distance_inc(mover, thing) # Then, move away from your target. distance_inc(mover, target) @@ -364,16 +378,16 @@ def get_range(obj1, obj2): range (int or None): Distance between two objects or None if not applicable """ # Return None if not applicable. - if not obj1.db.Combat_Range: + if not obj1.db.combat_range: return None - if not obj2.db.Combat_Range: + if not obj2.db.combat_range: return None - if obj1 not in obj2.db.Combat_Range: + if obj1 not in obj2.db.combat_range: return None - if obj2 not in obj1.db.Combat_Range: + if obj2 not in obj1.db.combat_range: return None # Return the range between the two objects. - return obj1.db.Combat_Range[obj2] + return obj1.db.combat_range[obj2] def combat_cleanup(character): """ @@ -401,7 +415,7 @@ def is_in_combat(character): Returns: (bool): True if in combat or False if not in combat """ - if character.db.Combat_TurnHandler: + if character.db.combat_turnhandler: return True return False @@ -416,7 +430,7 @@ def is_turn(character): Returns: (bool): True if it is their turn or False otherwise """ - turnhandler = character.db.Combat_TurnHandler + turnhandler = character.db.combat_turnhandler currentchar = turnhandler.db.fighters[turnhandler.db.turn] if character == currentchar: return True @@ -436,14 +450,14 @@ def spend_action(character, actions, action_name=None): combat to provided string """ if action_name: - character.db.Combat_LastAction = action_name + character.db.combat_lastaction = action_name if actions == 'all': # If spending all actions - character.db.Combat_ActionsLeft = 0 # Set actions to 0 + character.db.combat_actionsleft = 0 # Set actions to 0 else: - character.db.Combat_ActionsLeft -= actions # Use up actions. - if character.db.Combat_ActionsLeft < 0: - character.db.Combat_ActionsLeft = 0 # Can't have fewer than 0 actions - character.db.Combat_TurnHandler.turn_end_check(character) # Signal potential end of turn. + character.db.combat_actionsleft -= actions # Use up actions. + if character.db.combat_actionsleft < 0: + character.db.combat_actionsleft = 0 # Can't have fewer than 0 actions + character.db.combat_turnhandler.turn_end_check(character) # Signal potential end of turn. def combat_status_message(fighter): """ @@ -462,13 +476,13 @@ def combat_status_message(fighter): reach_obj = [] range_obj = [] - for object in fighter.db.Combat_Range: + for object in fighter.db.combat_range: if object != fighter: - if fighter.db.Combat_Range[object] == 0: + if fighter.db.combat_range[object] == 0: engaged_obj.append(object) - if fighter.db.Combat_Range[object] == 1: + if fighter.db.combat_range[object] == 1: reach_obj.append(object) - if fighter.db.Combat_Range[object] > 1: + if fighter.db.combat_range[object] > 1: range_obj.append(object) if engaged_obj: @@ -582,10 +596,10 @@ class TBRangeObject(DefaultObject): """ # If dropper is currently in combat - if dropper.location.db.Combat_TurnHandler: + if dropper.location.db.combat_turnhandler: # Object joins the range field - self.db.Combat_Range = {} - dropper.location.db.Combat_TurnHandler.join_rangefield(self, anchor_obj=dropper) + self.db.combat_range = {} + dropper.location.db.combat_turnhandler.join_rangefield(self, anchor_obj=dropper) def at_before_get(self, getter): """ Called by the default `get` command before this object has been @@ -628,13 +642,13 @@ class TBRangeObject(DefaultObject): """ # If gotten, erase range values - if self.db.Combat_Range: - del self.db.Combat_Range + if self.db.combat_range: + del self.db.combat_range # Remove this object from everyone's range fields for object in getter.location.contents: - if object.db.Combat_Range: - if self in object.db.Combat_Range: - object.db.Combat_Range.pop(self, None) + if object.db.combat_range: + if self in object.db.combat_range: + object.db.combat_range.pop(self, None) # If in combat, getter spends an action if is_in_combat(getter): spend_action(getter, 1, action_name="get") # Use up one action. @@ -726,9 +740,9 @@ class CmdFight(Command): if len(fighters) <= 1: # If you're the only able fighter in the room self.caller.msg("There's nobody here to fight!") return - if here.db.Combat_TurnHandler: # If there's already a fight going on... + if here.db.combat_turnhandler: # If there's already a fight going on... here.msg_contents("%s joins the fight!" % self.caller) - here.db.Combat_TurnHandler.join_fight(self.caller) # Join the fight! + here.db.combat_turnhandler.join_fight(self.caller) # Join the fight! return here.msg_contents("%s starts a fight!" % self.caller) # Add a turn handler script to the room, which starts combat. @@ -839,7 +853,7 @@ class CmdShoot(Command): # Test to see if there are any nearby enemy targets. in_melee = [] - for target in attacker.db.Combat_Range: + for target in attacker.db.combat_range: # Object is engaged and has HP if get_range(attacker, defender) == 0 and target.db.hp and target != self.caller: in_melee.append(target) # Add to list of targets in melee @@ -887,7 +901,7 @@ class CmdApproach(Command): if not target: # No valid target given. return - if not target.db.Combat_Range: # Target object is not on the range field + if not target.db.combat_range: # Target object is not on the range field self.caller.msg("You can't move toward that!") return @@ -938,7 +952,7 @@ class CmdWithdraw(Command): if not target: # No valid target given. return - if not target.db.Combat_Range: # Target object is not on the range field + if not target.db.combat_range: # Target object is not on the range field self.caller.msg("You can't move away from that!") return @@ -946,7 +960,7 @@ class CmdWithdraw(Command): self.caller.msg("You can't move away from yourself!") return - if mover.db.Combat_Range[target] >= 3: # Already at maximum distance + if mover.db.combat_range[target] >= 3: # Already at maximum distance self.caller.msg("You're as far as you can get from that target!") return @@ -1159,7 +1173,7 @@ class TBRangeTurnHandler(DefaultScript): self.initialize_for_combat(fighter) # Add a reference to this script to the room - self.obj.db.Combat_TurnHandler = self + self.obj.db.combat_turnhandler = self # Initialize range field for all objects in the room for object in self.obj.contents: @@ -1186,7 +1200,7 @@ class TBRangeTurnHandler(DefaultScript): """ for object in self.obj.contents: combat_cleanup(object) # Clean up the combat attributes for every object in the room. - self.obj.db.Combat_TurnHandler = None # Remove reference to turn handler in location + self.obj.db.combat_turnhandler = None # Remove reference to turn handler in location def at_repeat(self): """ @@ -1226,7 +1240,7 @@ class TBRangeTurnHandler(DefaultScript): else: # Start objects at range 1 from other objects rangedict.update({object:1}) - to_init.db.Combat_Range = rangedict + to_init.db.combat_range = rangedict def join_rangefield(self, to_init, anchor_obj=None, add_distance=0): """ @@ -1247,13 +1261,13 @@ class TBRangeTurnHandler(DefaultScript): if not anchor_obj: anchor_obj = contents[randint(0, (len(contents)-1))] # Copy the range values from the anchor object. - to_init.db.Combat_Range = anchor_obj.db.Combat_Range + to_init.db.combat_range = anchor_obj.db.combat_range # Add the new object to everyone else's ranges. for object in contents: - new_objects_range = object.db.Combat_Range[anchor_obj] - object.db.Combat_Range.update({to_init:new_objects_range}) + new_objects_range = object.db.combat_range[anchor_obj] + object.db.combat_range.update({to_init:new_objects_range}) # Set the new object's range to itself to 0. - to_init.db.Combat_Range.update({to_init:0}) + to_init.db.combat_range.update({to_init:0}) # Add additional distance from anchor object, if any. for n in range(add_distance): withdraw(to_init, anchor_obj) @@ -1266,9 +1280,9 @@ class TBRangeTurnHandler(DefaultScript): character (obj): Character to initialize for combat. """ combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case. - character.db.Combat_ActionsLeft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0 - character.db.Combat_TurnHandler = self # Add a reference to this turn handler script to the character - character.db.Combat_LastAction = "null" # Track last action taken in combat + character.db.combat_actionsleft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0 + character.db.combat_turnhandler = self # Add a reference to this turn handler script to the character + character.db.combat_lastaction = "null" # Track last action taken in combat def start_turn(self, character): """ @@ -1283,7 +1297,7 @@ class TBRangeTurnHandler(DefaultScript): characters to both move and attack in the same turn (or, alternately, move twice or attack twice). """ - character.db.Combat_ActionsLeft = 2 # 2 actions per turn. + character.db.combat_actionsleft = 2 # 2 actions per turn. # Prompt the character for their turn and give some information. character.msg("|wIt's your turn!|n") combat_status_message(character) @@ -1296,7 +1310,7 @@ class TBRangeTurnHandler(DefaultScript): # Check to see if every character disengaged as their last action. If so, end combat. disengage_check = True for fighter in self.db.fighters: - if fighter.db.Combat_LastAction != "disengage": # If a character has done anything but disengage + if fighter.db.combat_lastaction != "disengage": # If a character has done anything but disengage disengage_check = False if disengage_check: # All characters have disengaged self.obj.msg_contents("All fighters have disengaged! Combat is over!") @@ -1334,7 +1348,7 @@ class TBRangeTurnHandler(DefaultScript): Args: character (obj): Character to test for end of turn """ - if not character.db.Combat_ActionsLeft: # Character has no actions remaining + if not character.db.combat_actionsleft: # Character has no actions remaining self.next_turn() return @@ -1352,5 +1366,5 @@ class TBRangeTurnHandler(DefaultScript): # Initialize the character like you do at the start. self.initialize_for_combat(character) # Add the character to the rangefield, at range from everyone, if they're not on it already. - if not character.db.Combat_Range: + if not character.db.combat_range: self.join_rangefield(character, add_distance=2) From 347f161d94c420e5f1bbecd2512ee03289a34adf Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 23 Oct 2017 21:16:19 -0700 Subject: [PATCH 27/68] Simplified bool returns in is_in_combat, is_turn --- evennia/contrib/turnbattle/tb_basic.py | 8 ++------ evennia/contrib/turnbattle/tb_equip.py | 8 ++------ evennia/contrib/turnbattle/tb_range.py | 8 ++------ 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_basic.py b/evennia/contrib/turnbattle/tb_basic.py index 22a96016c8..e392715238 100644 --- a/evennia/contrib/turnbattle/tb_basic.py +++ b/evennia/contrib/turnbattle/tb_basic.py @@ -239,9 +239,7 @@ def is_in_combat(character): Returns: (bool): True if in combat or False if not in combat """ - if character.db.combat_turnhandler: - return True - return False + return bool(character.db.combat_turnhandler) def is_turn(character): @@ -256,9 +254,7 @@ def is_turn(character): """ turnhandler = character.db.combat_turnhandler currentchar = turnhandler.db.fighters[turnhandler.db.turn] - if character == currentchar: - return True - return False + return bool(character == currentchar) def spend_action(character, actions, action_name=None): diff --git a/evennia/contrib/turnbattle/tb_equip.py b/evennia/contrib/turnbattle/tb_equip.py index b415f82428..e3820393c4 100644 --- a/evennia/contrib/turnbattle/tb_equip.py +++ b/evennia/contrib/turnbattle/tb_equip.py @@ -284,9 +284,7 @@ def is_in_combat(character): Returns: (bool): True if in combat or False if not in combat """ - if character.db.combat_turnhandler: - return True - return False + return bool(character.db.combat_turnhandler) def is_turn(character): @@ -301,9 +299,7 @@ def is_turn(character): """ turnhandler = character.db.combat_turnhandler currentchar = turnhandler.db.fighters[turnhandler.db.turn] - if character == currentchar: - return True - return False + return bool(character == currentchar) def spend_action(character, actions, action_name=None): diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index 27566d4d27..91ccc3836e 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -415,9 +415,7 @@ def is_in_combat(character): Returns: (bool): True if in combat or False if not in combat """ - if character.db.combat_turnhandler: - return True - return False + return bool(character.db.combat_turnhandler) def is_turn(character): @@ -432,9 +430,7 @@ def is_turn(character): """ turnhandler = character.db.combat_turnhandler currentchar = turnhandler.db.fighters[turnhandler.db.turn] - if character == currentchar: - return True - return False + return bool(character == currentchar) def spend_action(character, actions, action_name=None): From f5760057722199c6823675cd4634e547f4088f23 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 23 Oct 2017 21:24:20 -0700 Subject: [PATCH 28/68] Moved turn handler script up higher in module --- evennia/contrib/turnbattle/tb_basic.py | 353 +++++++++---------- evennia/contrib/turnbattle/tb_equip.py | 350 +++++++++--------- evennia/contrib/turnbattle/tb_range.py | 470 ++++++++++++------------- 3 files changed, 584 insertions(+), 589 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_basic.py b/evennia/contrib/turnbattle/tb_basic.py index e392715238..d01ad7ed4a 100644 --- a/evennia/contrib/turnbattle/tb_basic.py +++ b/evennia/contrib/turnbattle/tb_basic.py @@ -333,7 +333,182 @@ class TBBasicCharacter(DefaultCharacter): return False return True +""" +---------------------------------------------------------------------------- +SCRIPTS START HERE +---------------------------------------------------------------------------- +""" + +class TBBasicTurnHandler(DefaultScript): + """ + This is the script that handles the progression of combat through turns. + On creation (when a fight is started) it adds all combat-ready characters + to its roster and then sorts them into a turn order. There can only be one + fight going on in a single room at a time, so the script is assigned to a + room as its object. + + Fights persist until only one participant is left with any HP or all + remaining participants choose to end the combat with the 'disengage' command. + """ + + def at_script_creation(self): + """ + Called once, when the script is created. + """ + self.key = "Combat Turn Handler" + self.interval = 5 # Once every 5 seconds + self.persistent = True + self.db.fighters = [] + + # Add all fighters in the room with at least 1 HP to the combat." + for object in self.obj.contents: + if object.db.hp: + self.db.fighters.append(object) + + # Initialize each fighter for combat + for fighter in self.db.fighters: + self.initialize_for_combat(fighter) + + # Add a reference to this script to the room + self.obj.db.combat_turnhandler = self + + # Roll initiative and sort the list of fighters depending on who rolls highest to determine turn order. + # The initiative roll is determined by the roll_init function and can be customized easily. + ordered_by_roll = sorted(self.db.fighters, key=roll_init, reverse=True) + self.db.fighters = ordered_by_roll + + # Announce the turn order. + self.obj.msg_contents("Turn order is: %s " % ", ".join(obj.key for obj in self.db.fighters)) + + # Start first fighter's turn. + self.start_turn(self.db.fighters[0]) + + # Set up the current turn and turn timeout delay. + self.db.turn = 0 + self.db.timer = 30 # 30 seconds + + def at_stop(self): + """ + Called at script termination. + """ + for fighter in self.db.fighters: + combat_cleanup(fighter) # Clean up the combat attributes for every fighter. + self.obj.db.combat_turnhandler = None # Remove reference to turn handler in location + + def at_repeat(self): + """ + Called once every self.interval seconds. + """ + currentchar = self.db.fighters[self.db.turn] # Note the current character in the turn order. + self.db.timer -= self.interval # Count down the timer. + + if self.db.timer <= 0: + # Force current character to disengage if timer runs out. + self.obj.msg_contents("%s's turn timed out!" % currentchar) + spend_action(currentchar, 'all', action_name="disengage") # Spend all remaining actions. + return + elif self.db.timer <= 10 and not self.db.timeout_warning_given: # 10 seconds left + # Warn the current character if they're about to time out. + currentchar.msg("WARNING: About to time out!") + self.db.timeout_warning_given = True + + def initialize_for_combat(self, character): + """ + Prepares a character for combat when starting or entering a fight. + + Args: + character (obj): Character to initialize for combat. + """ + combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case. + character.db.combat_actionsleft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0 + character.db.combat_turnhandler = self # Add a reference to this turn handler script to the character + character.db.combat_lastaction = "null" # Track last action taken in combat + + def start_turn(self, character): + """ + Readies a character for the start of their turn by replenishing their + available actions and notifying them that their turn has come up. + + Args: + character (obj): Character to be readied. + + Notes: + Here, you only get one action per turn, but you might want to allow more than + one per turn, or even grant a number of actions based on a character's + attributes. You can even add multiple different kinds of actions, I.E. actions + separated for movement, by adding "character.db.combat_movesleft = 3" or + something similar. + """ + character.db.combat_actionsleft = 1 # 1 action per turn. + # Prompt the character for their turn and give some information. + character.msg("|wIt's your turn! You have %i HP remaining.|n" % character.db.hp) + + def next_turn(self): + """ + Advances to the next character in the turn order. + """ + + # Check to see if every character disengaged as their last action. If so, end combat. + disengage_check = True + for fighter in self.db.fighters: + if fighter.db.combat_lastaction != "disengage": # If a character has done anything but disengage + disengage_check = False + if disengage_check: # All characters have disengaged + self.obj.msg_contents("All fighters have disengaged! Combat is over!") + self.stop() # Stop this script and end combat. + return + + # Check to see if only one character is left standing. If so, end combat. + defeated_characters = 0 + for fighter in self.db.fighters: + if fighter.db.HP == 0: + defeated_characters += 1 # Add 1 for every fighter with 0 HP left (defeated) + if defeated_characters == (len(self.db.fighters) - 1): # If only one character isn't defeated + for fighter in self.db.fighters: + if fighter.db.HP != 0: + LastStanding = fighter # Pick the one fighter left with HP remaining + self.obj.msg_contents("Only %s remains! Combat is over!" % LastStanding) + self.stop() # Stop this script and end combat. + return + + # Cycle to the next turn. + currentchar = self.db.fighters[self.db.turn] + self.db.turn += 1 # Go to the next in the turn order. + if self.db.turn > len(self.db.fighters) - 1: + self.db.turn = 0 # Go back to the first in the turn order once you reach the end. + newchar = self.db.fighters[self.db.turn] # Note the new character + self.db.timer = 30 + self.time_until_next_repeat() # Reset the timer. + self.db.timeout_warning_given = False # Reset the timeout warning. + self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar)) + self.start_turn(newchar) # Start the new character's turn. + + def turn_end_check(self, character): + """ + Tests to see if a character's turn is over, and cycles to the next turn if it is. + + Args: + character (obj): Character to test for end of turn + """ + if not character.db.combat_actionsleft: # Character has no actions remaining + self.next_turn() + return + + def join_fight(self, character): + """ + Adds a new character to a fight already in progress. + + Args: + character (obj): Character to be added to the fight. + """ + # Inserts the fighter to the turn order, right behind whoever's turn it currently is. + self.db.fighters.insert(self.db.turn, character) + # Tick the turn counter forward one to compensate. + self.db.turn += 1 + # Initialize the character like you do at the start. + self.initialize_for_combat(character) + + """ ---------------------------------------------------------------------------- COMMANDS START HERE @@ -568,180 +743,4 @@ class BattleCmdSet(default_cmds.CharacterCmdSet): self.add(CmdRest()) self.add(CmdPass()) self.add(CmdDisengage()) - self.add(CmdCombatHelp()) - - -""" ----------------------------------------------------------------------------- -SCRIPTS START HERE ----------------------------------------------------------------------------- -""" - - -class TBBasicTurnHandler(DefaultScript): - """ - This is the script that handles the progression of combat through turns. - On creation (when a fight is started) it adds all combat-ready characters - to its roster and then sorts them into a turn order. There can only be one - fight going on in a single room at a time, so the script is assigned to a - room as its object. - - Fights persist until only one participant is left with any HP or all - remaining participants choose to end the combat with the 'disengage' command. - """ - - def at_script_creation(self): - """ - Called once, when the script is created. - """ - self.key = "Combat Turn Handler" - self.interval = 5 # Once every 5 seconds - self.persistent = True - self.db.fighters = [] - - # Add all fighters in the room with at least 1 HP to the combat." - for object in self.obj.contents: - if object.db.hp: - self.db.fighters.append(object) - - # Initialize each fighter for combat - for fighter in self.db.fighters: - self.initialize_for_combat(fighter) - - # Add a reference to this script to the room - self.obj.db.combat_turnhandler = self - - # Roll initiative and sort the list of fighters depending on who rolls highest to determine turn order. - # The initiative roll is determined by the roll_init function and can be customized easily. - ordered_by_roll = sorted(self.db.fighters, key=roll_init, reverse=True) - self.db.fighters = ordered_by_roll - - # Announce the turn order. - self.obj.msg_contents("Turn order is: %s " % ", ".join(obj.key for obj in self.db.fighters)) - - # Start first fighter's turn. - self.start_turn(self.db.fighters[0]) - - # Set up the current turn and turn timeout delay. - self.db.turn = 0 - self.db.timer = 30 # 30 seconds - - def at_stop(self): - """ - Called at script termination. - """ - for fighter in self.db.fighters: - combat_cleanup(fighter) # Clean up the combat attributes for every fighter. - self.obj.db.combat_turnhandler = None # Remove reference to turn handler in location - - def at_repeat(self): - """ - Called once every self.interval seconds. - """ - currentchar = self.db.fighters[self.db.turn] # Note the current character in the turn order. - self.db.timer -= self.interval # Count down the timer. - - if self.db.timer <= 0: - # Force current character to disengage if timer runs out. - self.obj.msg_contents("%s's turn timed out!" % currentchar) - spend_action(currentchar, 'all', action_name="disengage") # Spend all remaining actions. - return - elif self.db.timer <= 10 and not self.db.timeout_warning_given: # 10 seconds left - # Warn the current character if they're about to time out. - currentchar.msg("WARNING: About to time out!") - self.db.timeout_warning_given = True - - def initialize_for_combat(self, character): - """ - Prepares a character for combat when starting or entering a fight. - - Args: - character (obj): Character to initialize for combat. - """ - combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case. - character.db.combat_actionsleft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0 - character.db.combat_turnhandler = self # Add a reference to this turn handler script to the character - character.db.combat_lastaction = "null" # Track last action taken in combat - - def start_turn(self, character): - """ - Readies a character for the start of their turn by replenishing their - available actions and notifying them that their turn has come up. - - Args: - character (obj): Character to be readied. - - Notes: - Here, you only get one action per turn, but you might want to allow more than - one per turn, or even grant a number of actions based on a character's - attributes. You can even add multiple different kinds of actions, I.E. actions - separated for movement, by adding "character.db.combat_movesleft = 3" or - something similar. - """ - character.db.combat_actionsleft = 1 # 1 action per turn. - # Prompt the character for their turn and give some information. - character.msg("|wIt's your turn! You have %i HP remaining.|n" % character.db.hp) - - def next_turn(self): - """ - Advances to the next character in the turn order. - """ - - # Check to see if every character disengaged as their last action. If so, end combat. - disengage_check = True - for fighter in self.db.fighters: - if fighter.db.combat_lastaction != "disengage": # If a character has done anything but disengage - disengage_check = False - if disengage_check: # All characters have disengaged - self.obj.msg_contents("All fighters have disengaged! Combat is over!") - self.stop() # Stop this script and end combat. - return - - # Check to see if only one character is left standing. If so, end combat. - defeated_characters = 0 - for fighter in self.db.fighters: - if fighter.db.HP == 0: - defeated_characters += 1 # Add 1 for every fighter with 0 HP left (defeated) - if defeated_characters == (len(self.db.fighters) - 1): # If only one character isn't defeated - for fighter in self.db.fighters: - if fighter.db.HP != 0: - LastStanding = fighter # Pick the one fighter left with HP remaining - self.obj.msg_contents("Only %s remains! Combat is over!" % LastStanding) - self.stop() # Stop this script and end combat. - return - - # Cycle to the next turn. - currentchar = self.db.fighters[self.db.turn] - self.db.turn += 1 # Go to the next in the turn order. - if self.db.turn > len(self.db.fighters) - 1: - self.db.turn = 0 # Go back to the first in the turn order once you reach the end. - newchar = self.db.fighters[self.db.turn] # Note the new character - self.db.timer = 30 + self.time_until_next_repeat() # Reset the timer. - self.db.timeout_warning_given = False # Reset the timeout warning. - self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar)) - self.start_turn(newchar) # Start the new character's turn. - - def turn_end_check(self, character): - """ - Tests to see if a character's turn is over, and cycles to the next turn if it is. - - Args: - character (obj): Character to test for end of turn - """ - if not character.db.combat_actionsleft: # Character has no actions remaining - self.next_turn() - return - - def join_fight(self, character): - """ - Adds a new character to a fight already in progress. - - Args: - character (obj): Character to be added to the fight. - """ - # Inserts the fighter to the turn order, right behind whoever's turn it currently is. - self.db.fighters.insert(self.db.turn, character) - # Tick the turn counter forward one to compensate. - self.db.turn += 1 - # Initialize the character like you do at the start. - self.initialize_for_combat(character) + self.add(CmdCombatHelp()) \ No newline at end of file diff --git a/evennia/contrib/turnbattle/tb_equip.py b/evennia/contrib/turnbattle/tb_equip.py index e3820393c4..41fa93d4dd 100644 --- a/evennia/contrib/turnbattle/tb_equip.py +++ b/evennia/contrib/turnbattle/tb_equip.py @@ -324,6 +324,180 @@ def spend_action(character, actions, action_name=None): character.db.combat_actionsleft = 0 # Can't have fewer than 0 actions character.db.combat_turnhandler.turn_end_check(character) # Signal potential end of turn. +""" +---------------------------------------------------------------------------- +SCRIPTS START HERE +---------------------------------------------------------------------------- +""" + + +class TBEquipTurnHandler(DefaultScript): + """ + This is the script that handles the progression of combat through turns. + On creation (when a fight is started) it adds all combat-ready characters + to its roster and then sorts them into a turn order. There can only be one + fight going on in a single room at a time, so the script is assigned to a + room as its object. + + Fights persist until only one participant is left with any HP or all + remaining participants choose to end the combat with the 'disengage' command. + """ + + def at_script_creation(self): + """ + Called once, when the script is created. + """ + self.key = "Combat Turn Handler" + self.interval = 5 # Once every 5 seconds + self.persistent = True + self.db.fighters = [] + + # Add all fighters in the room with at least 1 HP to the combat." + for object in self.obj.contents: + if object.db.hp: + self.db.fighters.append(object) + + # Initialize each fighter for combat + for fighter in self.db.fighters: + self.initialize_for_combat(fighter) + + # Add a reference to this script to the room + self.obj.db.combat_turnhandler = self + + # Roll initiative and sort the list of fighters depending on who rolls highest to determine turn order. + # The initiative roll is determined by the roll_init function and can be customized easily. + ordered_by_roll = sorted(self.db.fighters, key=roll_init, reverse=True) + self.db.fighters = ordered_by_roll + + # Announce the turn order. + self.obj.msg_contents("Turn order is: %s " % ", ".join(obj.key for obj in self.db.fighters)) + + # Start first fighter's turn. + self.start_turn(self.db.fighters[0]) + + # Set up the current turn and turn timeout delay. + self.db.turn = 0 + self.db.timer = 30 # 30 seconds + + def at_stop(self): + """ + Called at script termination. + """ + for fighter in self.db.fighters: + combat_cleanup(fighter) # Clean up the combat attributes for every fighter. + self.obj.db.combat_turnhandler = None # Remove reference to turn handler in location + + def at_repeat(self): + """ + Called once every self.interval seconds. + """ + currentchar = self.db.fighters[self.db.turn] # Note the current character in the turn order. + self.db.timer -= self.interval # Count down the timer. + + if self.db.timer <= 0: + # Force current character to disengage if timer runs out. + self.obj.msg_contents("%s's turn timed out!" % currentchar) + spend_action(currentchar, 'all', action_name="disengage") # Spend all remaining actions. + return + elif self.db.timer <= 10 and not self.db.timeout_warning_given: # 10 seconds left + # Warn the current character if they're about to time out. + currentchar.msg("WARNING: About to time out!") + self.db.timeout_warning_given = True + + def initialize_for_combat(self, character): + """ + Prepares a character for combat when starting or entering a fight. + + Args: + character (obj): Character to initialize for combat. + """ + combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case. + character.db.combat_actionsleft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0 + character.db.combat_turnhandler = self # Add a reference to this turn handler script to the character + character.db.combat_lastaction = "null" # Track last action taken in combat + + def start_turn(self, character): + """ + Readies a character for the start of their turn by replenishing their + available actions and notifying them that their turn has come up. + + Args: + character (obj): Character to be readied. + + Notes: + Here, you only get one action per turn, but you might want to allow more than + one per turn, or even grant a number of actions based on a character's + attributes. You can even add multiple different kinds of actions, I.E. actions + separated for movement, by adding "character.db.combat_movesleft = 3" or + something similar. + """ + character.db.combat_actionsleft = 1 # 1 action per turn. + # Prompt the character for their turn and give some information. + character.msg("|wIt's your turn! You have %i HP remaining.|n" % character.db.hp) + + def next_turn(self): + """ + Advances to the next character in the turn order. + """ + + # Check to see if every character disengaged as their last action. If so, end combat. + disengage_check = True + for fighter in self.db.fighters: + if fighter.db.combat_lastaction != "disengage": # If a character has done anything but disengage + disengage_check = False + if disengage_check: # All characters have disengaged + self.obj.msg_contents("All fighters have disengaged! Combat is over!") + self.stop() # Stop this script and end combat. + return + + # Check to see if only one character is left standing. If so, end combat. + defeated_characters = 0 + for fighter in self.db.fighters: + if fighter.db.HP == 0: + defeated_characters += 1 # Add 1 for every fighter with 0 HP left (defeated) + if defeated_characters == (len(self.db.fighters) - 1): # If only one character isn't defeated + for fighter in self.db.fighters: + if fighter.db.HP != 0: + LastStanding = fighter # Pick the one fighter left with HP remaining + self.obj.msg_contents("Only %s remains! Combat is over!" % LastStanding) + self.stop() # Stop this script and end combat. + return + + # Cycle to the next turn. + currentchar = self.db.fighters[self.db.turn] + self.db.turn += 1 # Go to the next in the turn order. + if self.db.turn > len(self.db.fighters) - 1: + self.db.turn = 0 # Go back to the first in the turn order once you reach the end. + newchar = self.db.fighters[self.db.turn] # Note the new character + self.db.timer = 30 + self.time_until_next_repeat() # Reset the timer. + self.db.timeout_warning_given = False # Reset the timeout warning. + self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar)) + self.start_turn(newchar) # Start the new character's turn. + + def turn_end_check(self, character): + """ + Tests to see if a character's turn is over, and cycles to the next turn if it is. + + Args: + character (obj): Character to test for end of turn + """ + if not character.db.combat_actionsleft: # Character has no actions remaining + self.next_turn() + return + + def join_fight(self, character): + """ + Adds a new character to a fight already in progress. + + Args: + character (obj): Character to be added to the fight. + """ + # Inserts the fighter to the turn order, right behind whoever's turn it currently is. + self.db.fighters.insert(self.db.turn, character) + # Tick the turn counter forward one to compensate. + self.db.turn += 1 + # Initialize the character like you do at the start. + self.initialize_for_combat(character) """ ---------------------------------------------------------------------------- @@ -844,182 +1018,6 @@ class BattleCmdSet(default_cmds.CharacterCmdSet): self.add(CmdDon()) self.add(CmdDoff()) - -""" ----------------------------------------------------------------------------- -SCRIPTS START HERE ----------------------------------------------------------------------------- -""" - - -class TBEquipTurnHandler(DefaultScript): - """ - This is the script that handles the progression of combat through turns. - On creation (when a fight is started) it adds all combat-ready characters - to its roster and then sorts them into a turn order. There can only be one - fight going on in a single room at a time, so the script is assigned to a - room as its object. - - Fights persist until only one participant is left with any HP or all - remaining participants choose to end the combat with the 'disengage' command. - """ - - def at_script_creation(self): - """ - Called once, when the script is created. - """ - self.key = "Combat Turn Handler" - self.interval = 5 # Once every 5 seconds - self.persistent = True - self.db.fighters = [] - - # Add all fighters in the room with at least 1 HP to the combat." - for object in self.obj.contents: - if object.db.hp: - self.db.fighters.append(object) - - # Initialize each fighter for combat - for fighter in self.db.fighters: - self.initialize_for_combat(fighter) - - # Add a reference to this script to the room - self.obj.db.combat_turnhandler = self - - # Roll initiative and sort the list of fighters depending on who rolls highest to determine turn order. - # The initiative roll is determined by the roll_init function and can be customized easily. - ordered_by_roll = sorted(self.db.fighters, key=roll_init, reverse=True) - self.db.fighters = ordered_by_roll - - # Announce the turn order. - self.obj.msg_contents("Turn order is: %s " % ", ".join(obj.key for obj in self.db.fighters)) - - # Start first fighter's turn. - self.start_turn(self.db.fighters[0]) - - # Set up the current turn and turn timeout delay. - self.db.turn = 0 - self.db.timer = 30 # 30 seconds - - def at_stop(self): - """ - Called at script termination. - """ - for fighter in self.db.fighters: - combat_cleanup(fighter) # Clean up the combat attributes for every fighter. - self.obj.db.combat_turnhandler = None # Remove reference to turn handler in location - - def at_repeat(self): - """ - Called once every self.interval seconds. - """ - currentchar = self.db.fighters[self.db.turn] # Note the current character in the turn order. - self.db.timer -= self.interval # Count down the timer. - - if self.db.timer <= 0: - # Force current character to disengage if timer runs out. - self.obj.msg_contents("%s's turn timed out!" % currentchar) - spend_action(currentchar, 'all', action_name="disengage") # Spend all remaining actions. - return - elif self.db.timer <= 10 and not self.db.timeout_warning_given: # 10 seconds left - # Warn the current character if they're about to time out. - currentchar.msg("WARNING: About to time out!") - self.db.timeout_warning_given = True - - def initialize_for_combat(self, character): - """ - Prepares a character for combat when starting or entering a fight. - - Args: - character (obj): Character to initialize for combat. - """ - combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case. - character.db.combat_actionsleft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0 - character.db.combat_turnhandler = self # Add a reference to this turn handler script to the character - character.db.combat_lastaction = "null" # Track last action taken in combat - - def start_turn(self, character): - """ - Readies a character for the start of their turn by replenishing their - available actions and notifying them that their turn has come up. - - Args: - character (obj): Character to be readied. - - Notes: - Here, you only get one action per turn, but you might want to allow more than - one per turn, or even grant a number of actions based on a character's - attributes. You can even add multiple different kinds of actions, I.E. actions - separated for movement, by adding "character.db.combat_movesleft = 3" or - something similar. - """ - character.db.combat_actionsleft = 1 # 1 action per turn. - # Prompt the character for their turn and give some information. - character.msg("|wIt's your turn! You have %i HP remaining.|n" % character.db.hp) - - def next_turn(self): - """ - Advances to the next character in the turn order. - """ - - # Check to see if every character disengaged as their last action. If so, end combat. - disengage_check = True - for fighter in self.db.fighters: - if fighter.db.combat_lastaction != "disengage": # If a character has done anything but disengage - disengage_check = False - if disengage_check: # All characters have disengaged - self.obj.msg_contents("All fighters have disengaged! Combat is over!") - self.stop() # Stop this script and end combat. - return - - # Check to see if only one character is left standing. If so, end combat. - defeated_characters = 0 - for fighter in self.db.fighters: - if fighter.db.HP == 0: - defeated_characters += 1 # Add 1 for every fighter with 0 HP left (defeated) - if defeated_characters == (len(self.db.fighters) - 1): # If only one character isn't defeated - for fighter in self.db.fighters: - if fighter.db.HP != 0: - LastStanding = fighter # Pick the one fighter left with HP remaining - self.obj.msg_contents("Only %s remains! Combat is over!" % LastStanding) - self.stop() # Stop this script and end combat. - return - - # Cycle to the next turn. - currentchar = self.db.fighters[self.db.turn] - self.db.turn += 1 # Go to the next in the turn order. - if self.db.turn > len(self.db.fighters) - 1: - self.db.turn = 0 # Go back to the first in the turn order once you reach the end. - newchar = self.db.fighters[self.db.turn] # Note the new character - self.db.timer = 30 + self.time_until_next_repeat() # Reset the timer. - self.db.timeout_warning_given = False # Reset the timeout warning. - self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar)) - self.start_turn(newchar) # Start the new character's turn. - - def turn_end_check(self, character): - """ - Tests to see if a character's turn is over, and cycles to the next turn if it is. - - Args: - character (obj): Character to test for end of turn - """ - if not character.db.combat_actionsleft: # Character has no actions remaining - self.next_turn() - return - - def join_fight(self, character): - """ - Adds a new character to a fight already in progress. - - Args: - character (obj): Character to be added to the fight. - """ - # Inserts the fighter to the turn order, right behind whoever's turn it currently is. - self.db.fighters.insert(self.db.turn, character) - # Tick the turn counter forward one to compensate. - self.db.turn += 1 - # Initialize the character like you do at the start. - self.initialize_for_combat(character) - """ ---------------------------------------------------------------------------- PROTOTYPES START HERE diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index 91ccc3836e..daeb4bad8f 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -491,7 +491,240 @@ def combat_status_message(fighter): fighter.msg(status_msg) return +""" +---------------------------------------------------------------------------- +SCRIPTS START HERE +---------------------------------------------------------------------------- +""" + + +class TBRangeTurnHandler(DefaultScript): + """ + This is the script that handles the progression of combat through turns. + On creation (when a fight is started) it adds all combat-ready characters + to its roster and then sorts them into a turn order. There can only be one + fight going on in a single room at a time, so the script is assigned to a + room as its object. + + Fights persist until only one participant is left with any HP or all + remaining participants choose to end the combat with the 'disengage' command. + """ + + def at_script_creation(self): + """ + Called once, when the script is created. + """ + self.key = "Combat Turn Handler" + self.interval = 5 # Once every 5 seconds + self.persistent = True + self.db.fighters = [] + + # Add all fighters in the room with at least 1 HP to the combat." + for object in self.obj.contents: + if object.db.hp: + self.db.fighters.append(object) + + # Initialize each fighter for combat + for fighter in self.db.fighters: + self.initialize_for_combat(fighter) + + # Add a reference to this script to the room + self.obj.db.combat_turnhandler = self + # Initialize range field for all objects in the room + for object in self.obj.contents: + self.init_range(object) + + # Roll initiative and sort the list of fighters depending on who rolls highest to determine turn order. + # The initiative roll is determined by the roll_init function and can be customized easily. + ordered_by_roll = sorted(self.db.fighters, key=roll_init, reverse=True) + self.db.fighters = ordered_by_roll + + # Announce the turn order. + self.obj.msg_contents("Turn order is: %s " % ", ".join(obj.key for obj in self.db.fighters)) + + # Start first fighter's turn. + self.start_turn(self.db.fighters[0]) + + # Set up the current turn and turn timeout delay. + self.db.turn = 0 + self.db.timer = 30 # 30 seconds + + def at_stop(self): + """ + Called at script termination. + """ + for object in self.obj.contents: + combat_cleanup(object) # Clean up the combat attributes for every object in the room. + self.obj.db.combat_turnhandler = None # Remove reference to turn handler in location + + def at_repeat(self): + """ + Called once every self.interval seconds. + """ + currentchar = self.db.fighters[self.db.turn] # Note the current character in the turn order. + self.db.timer -= self.interval # Count down the timer. + + if self.db.timer <= 0: + # Force current character to disengage if timer runs out. + self.obj.msg_contents("%s's turn timed out!" % currentchar) + spend_action(currentchar, 'all', action_name="disengage") # Spend all remaining actions. + return + elif self.db.timer <= 10 and not self.db.timeout_warning_given: # 10 seconds left + # Warn the current character if they're about to time out. + currentchar.msg("WARNING: About to time out!") + self.db.timeout_warning_given = True + + def init_range(self, to_init): + """ + Initializes range values for an object at the start of a fight. + + Args: + to_init (object): Object to initialize range field for. + """ + rangedict = {} + # Get a list of objects in the room. + objectlist = self.obj.contents + for object in objectlist: + # Object always at distance 0 from itself + if object == to_init: + rangedict.update({object:0}) + else: + if object.destination or to_init.destination: + # Start exits at range 2 to put them at the 'edges' + rangedict.update({object:2}) + else: + # Start objects at range 1 from other objects + rangedict.update({object:1}) + to_init.db.combat_range = rangedict + + def join_rangefield(self, to_init, anchor_obj=None, add_distance=0): + """ + Adds a new object to the range field of a fight in progress. + + Args: + to_init (object): Object to initialize range field for. + + Kwargs: + anchor_obj (object): Object to copy range values from, or None for a random object. + add_distance (int): Distance to put between to_init object and anchor object. + + """ + # Get a list of room's contents without to_init object. + contents = self.obj.contents + contents.remove(to_init) + # If no anchor object given, pick one in the room at random. + if not anchor_obj: + anchor_obj = contents[randint(0, (len(contents)-1))] + # Copy the range values from the anchor object. + to_init.db.combat_range = anchor_obj.db.combat_range + # Add the new object to everyone else's ranges. + for object in contents: + new_objects_range = object.db.combat_range[anchor_obj] + object.db.combat_range.update({to_init:new_objects_range}) + # Set the new object's range to itself to 0. + to_init.db.combat_range.update({to_init:0}) + # Add additional distance from anchor object, if any. + for n in range(add_distance): + withdraw(to_init, anchor_obj) + + def initialize_for_combat(self, character): + """ + Prepares a character for combat when starting or entering a fight. + + Args: + character (obj): Character to initialize for combat. + """ + combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case. + character.db.combat_actionsleft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0 + character.db.combat_turnhandler = self # Add a reference to this turn handler script to the character + character.db.combat_lastaction = "null" # Track last action taken in combat + + def start_turn(self, character): + """ + Readies a character for the start of their turn by replenishing their + available actions and notifying them that their turn has come up. + + Args: + character (obj): Character to be readied. + + Notes: + In this example, characters are given two actions per turn. This allows + characters to both move and attack in the same turn (or, alternately, + move twice or attack twice). + """ + character.db.combat_actionsleft = 2 # 2 actions per turn. + # Prompt the character for their turn and give some information. + character.msg("|wIt's your turn!|n") + combat_status_message(character) + + def next_turn(self): + """ + Advances to the next character in the turn order. + """ + + # Check to see if every character disengaged as their last action. If so, end combat. + disengage_check = True + for fighter in self.db.fighters: + if fighter.db.combat_lastaction != "disengage": # If a character has done anything but disengage + disengage_check = False + if disengage_check: # All characters have disengaged + self.obj.msg_contents("All fighters have disengaged! Combat is over!") + self.stop() # Stop this script and end combat. + return + + # Check to see if only one character is left standing. If so, end combat. + defeated_characters = 0 + for fighter in self.db.fighters: + if fighter.db.HP == 0: + defeated_characters += 1 # Add 1 for every fighter with 0 HP left (defeated) + if defeated_characters == (len(self.db.fighters) - 1): # If only one character isn't defeated + for fighter in self.db.fighters: + if fighter.db.HP != 0: + LastStanding = fighter # Pick the one fighter left with HP remaining + self.obj.msg_contents("Only %s remains! Combat is over!" % LastStanding) + self.stop() # Stop this script and end combat. + return + + # Cycle to the next turn. + currentchar = self.db.fighters[self.db.turn] + self.db.turn += 1 # Go to the next in the turn order. + if self.db.turn > len(self.db.fighters) - 1: + self.db.turn = 0 # Go back to the first in the turn order once you reach the end. + newchar = self.db.fighters[self.db.turn] # Note the new character + self.db.timer = 30 + self.time_until_next_repeat() # Reset the timer. + self.db.timeout_warning_given = False # Reset the timeout warning. + self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar)) + self.start_turn(newchar) # Start the new character's turn. + + def turn_end_check(self, character): + """ + Tests to see if a character's turn is over, and cycles to the next turn if it is. + + Args: + character (obj): Character to test for end of turn + """ + if not character.db.combat_actionsleft: # Character has no actions remaining + self.next_turn() + return + + def join_fight(self, character): + """ + Adds a new character to a fight already in progress. + + Args: + character (obj): Character to be added to the fight. + """ + # Inserts the fighter to the turn order, right behind whoever's turn it currently is. + self.db.fighters.insert(self.db.turn, character) + # Tick the turn counter forward one to compensate. + self.db.turn += 1 + # Initialize the character like you do at the start. + self.initialize_for_combat(character) + # Add the character to the rangefield, at range from everyone, if they're not on it already. + if not character.db.combat_range: + self.join_rangefield(character, add_distance=2) + """ ---------------------------------------------------------------------------- @@ -1128,239 +1361,4 @@ class BattleCmdSet(default_cmds.CharacterCmdSet): self.add(CmdApproach()) self.add(CmdWithdraw()) self.add(CmdStatus()) - self.add(CmdCombatHelp()) - - -""" ----------------------------------------------------------------------------- -SCRIPTS START HERE ----------------------------------------------------------------------------- -""" - - -class TBRangeTurnHandler(DefaultScript): - """ - This is the script that handles the progression of combat through turns. - On creation (when a fight is started) it adds all combat-ready characters - to its roster and then sorts them into a turn order. There can only be one - fight going on in a single room at a time, so the script is assigned to a - room as its object. - - Fights persist until only one participant is left with any HP or all - remaining participants choose to end the combat with the 'disengage' command. - """ - - def at_script_creation(self): - """ - Called once, when the script is created. - """ - self.key = "Combat Turn Handler" - self.interval = 5 # Once every 5 seconds - self.persistent = True - self.db.fighters = [] - - # Add all fighters in the room with at least 1 HP to the combat." - for object in self.obj.contents: - if object.db.hp: - self.db.fighters.append(object) - - # Initialize each fighter for combat - for fighter in self.db.fighters: - self.initialize_for_combat(fighter) - - # Add a reference to this script to the room - self.obj.db.combat_turnhandler = self - - # Initialize range field for all objects in the room - for object in self.obj.contents: - self.init_range(object) - - # Roll initiative and sort the list of fighters depending on who rolls highest to determine turn order. - # The initiative roll is determined by the roll_init function and can be customized easily. - ordered_by_roll = sorted(self.db.fighters, key=roll_init, reverse=True) - self.db.fighters = ordered_by_roll - - # Announce the turn order. - self.obj.msg_contents("Turn order is: %s " % ", ".join(obj.key for obj in self.db.fighters)) - - # Start first fighter's turn. - self.start_turn(self.db.fighters[0]) - - # Set up the current turn and turn timeout delay. - self.db.turn = 0 - self.db.timer = 30 # 30 seconds - - def at_stop(self): - """ - Called at script termination. - """ - for object in self.obj.contents: - combat_cleanup(object) # Clean up the combat attributes for every object in the room. - self.obj.db.combat_turnhandler = None # Remove reference to turn handler in location - - def at_repeat(self): - """ - Called once every self.interval seconds. - """ - currentchar = self.db.fighters[self.db.turn] # Note the current character in the turn order. - self.db.timer -= self.interval # Count down the timer. - - if self.db.timer <= 0: - # Force current character to disengage if timer runs out. - self.obj.msg_contents("%s's turn timed out!" % currentchar) - spend_action(currentchar, 'all', action_name="disengage") # Spend all remaining actions. - return - elif self.db.timer <= 10 and not self.db.timeout_warning_given: # 10 seconds left - # Warn the current character if they're about to time out. - currentchar.msg("WARNING: About to time out!") - self.db.timeout_warning_given = True - - def init_range(self, to_init): - """ - Initializes range values for an object at the start of a fight. - - Args: - to_init (object): Object to initialize range field for. - """ - rangedict = {} - # Get a list of objects in the room. - objectlist = self.obj.contents - for object in objectlist: - # Object always at distance 0 from itself - if object == to_init: - rangedict.update({object:0}) - else: - if object.destination or to_init.destination: - # Start exits at range 2 to put them at the 'edges' - rangedict.update({object:2}) - else: - # Start objects at range 1 from other objects - rangedict.update({object:1}) - to_init.db.combat_range = rangedict - - def join_rangefield(self, to_init, anchor_obj=None, add_distance=0): - """ - Adds a new object to the range field of a fight in progress. - - Args: - to_init (object): Object to initialize range field for. - - Kwargs: - anchor_obj (object): Object to copy range values from, or None for a random object. - add_distance (int): Distance to put between to_init object and anchor object. - - """ - # Get a list of room's contents without to_init object. - contents = self.obj.contents - contents.remove(to_init) - # If no anchor object given, pick one in the room at random. - if not anchor_obj: - anchor_obj = contents[randint(0, (len(contents)-1))] - # Copy the range values from the anchor object. - to_init.db.combat_range = anchor_obj.db.combat_range - # Add the new object to everyone else's ranges. - for object in contents: - new_objects_range = object.db.combat_range[anchor_obj] - object.db.combat_range.update({to_init:new_objects_range}) - # Set the new object's range to itself to 0. - to_init.db.combat_range.update({to_init:0}) - # Add additional distance from anchor object, if any. - for n in range(add_distance): - withdraw(to_init, anchor_obj) - - def initialize_for_combat(self, character): - """ - Prepares a character for combat when starting or entering a fight. - - Args: - character (obj): Character to initialize for combat. - """ - combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case. - character.db.combat_actionsleft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0 - character.db.combat_turnhandler = self # Add a reference to this turn handler script to the character - character.db.combat_lastaction = "null" # Track last action taken in combat - - def start_turn(self, character): - """ - Readies a character for the start of their turn by replenishing their - available actions and notifying them that their turn has come up. - - Args: - character (obj): Character to be readied. - - Notes: - In this example, characters are given two actions per turn. This allows - characters to both move and attack in the same turn (or, alternately, - move twice or attack twice). - """ - character.db.combat_actionsleft = 2 # 2 actions per turn. - # Prompt the character for their turn and give some information. - character.msg("|wIt's your turn!|n") - combat_status_message(character) - - def next_turn(self): - """ - Advances to the next character in the turn order. - """ - - # Check to see if every character disengaged as their last action. If so, end combat. - disengage_check = True - for fighter in self.db.fighters: - if fighter.db.combat_lastaction != "disengage": # If a character has done anything but disengage - disengage_check = False - if disengage_check: # All characters have disengaged - self.obj.msg_contents("All fighters have disengaged! Combat is over!") - self.stop() # Stop this script and end combat. - return - - # Check to see if only one character is left standing. If so, end combat. - defeated_characters = 0 - for fighter in self.db.fighters: - if fighter.db.HP == 0: - defeated_characters += 1 # Add 1 for every fighter with 0 HP left (defeated) - if defeated_characters == (len(self.db.fighters) - 1): # If only one character isn't defeated - for fighter in self.db.fighters: - if fighter.db.HP != 0: - LastStanding = fighter # Pick the one fighter left with HP remaining - self.obj.msg_contents("Only %s remains! Combat is over!" % LastStanding) - self.stop() # Stop this script and end combat. - return - - # Cycle to the next turn. - currentchar = self.db.fighters[self.db.turn] - self.db.turn += 1 # Go to the next in the turn order. - if self.db.turn > len(self.db.fighters) - 1: - self.db.turn = 0 # Go back to the first in the turn order once you reach the end. - newchar = self.db.fighters[self.db.turn] # Note the new character - self.db.timer = 30 + self.time_until_next_repeat() # Reset the timer. - self.db.timeout_warning_given = False # Reset the timeout warning. - self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar)) - self.start_turn(newchar) # Start the new character's turn. - - def turn_end_check(self, character): - """ - Tests to see if a character's turn is over, and cycles to the next turn if it is. - - Args: - character (obj): Character to test for end of turn - """ - if not character.db.combat_actionsleft: # Character has no actions remaining - self.next_turn() - return - - def join_fight(self, character): - """ - Adds a new character to a fight already in progress. - - Args: - character (obj): Character to be added to the fight. - """ - # Inserts the fighter to the turn order, right behind whoever's turn it currently is. - self.db.fighters.insert(self.db.turn, character) - # Tick the turn counter forward one to compensate. - self.db.turn += 1 - # Initialize the character like you do at the start. - self.initialize_for_combat(character) - # Add the character to the rangefield, at range from everyone, if they're not on it already. - if not character.db.combat_range: - self.join_rangefield(character, add_distance=2) + self.add(CmdCombatHelp()) \ No newline at end of file From 30eea75ad72eff7ee857cbd307abe035cca16166 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 23 Oct 2017 21:36:42 -0700 Subject: [PATCH 29/68] Options for turn timeout and actions per turn --- evennia/contrib/turnbattle/tb_basic.py | 16 ++++++++++++---- evennia/contrib/turnbattle/tb_equip.py | 16 ++++++++++++---- evennia/contrib/turnbattle/tb_range.py | 16 ++++++++++++---- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_basic.py b/evennia/contrib/turnbattle/tb_basic.py index d01ad7ed4a..017094d0ec 100644 --- a/evennia/contrib/turnbattle/tb_basic.py +++ b/evennia/contrib/turnbattle/tb_basic.py @@ -48,10 +48,18 @@ from evennia.commands.default.help import CmdHelp """ ---------------------------------------------------------------------------- -COMBAT FUNCTIONS START HERE +OPTIONS ---------------------------------------------------------------------------- """ +TURN_TIMEOUT = 30 # Time before turns automatically end, in seconds +ACTIONS_PER_TURN = 1 # Number of actions allowed per turn + +""" +---------------------------------------------------------------------------- +COMBAT FUNCTIONS START HERE +---------------------------------------------------------------------------- +""" def roll_init(character): """ @@ -386,7 +394,7 @@ class TBBasicTurnHandler(DefaultScript): # Set up the current turn and turn timeout delay. self.db.turn = 0 - self.db.timer = 30 # 30 seconds + self.db.timer = TURN_TIMEOUT # Set timer to turn timeout specified in options def at_stop(self): """ @@ -440,7 +448,7 @@ class TBBasicTurnHandler(DefaultScript): separated for movement, by adding "character.db.combat_movesleft = 3" or something similar. """ - character.db.combat_actionsleft = 1 # 1 action per turn. + character.db.combat_actionsleft = ACTIONS_PER_TURN # Replenish actions # Prompt the character for their turn and give some information. character.msg("|wIt's your turn! You have %i HP remaining.|n" % character.db.hp) @@ -478,7 +486,7 @@ class TBBasicTurnHandler(DefaultScript): if self.db.turn > len(self.db.fighters) - 1: self.db.turn = 0 # Go back to the first in the turn order once you reach the end. newchar = self.db.fighters[self.db.turn] # Note the new character - self.db.timer = 30 + self.time_until_next_repeat() # Reset the timer. + self.db.timer = TURN_TIMEOUT + self.time_until_next_repeat() # Reset the timer. self.db.timeout_warning_given = False # Reset the timeout warning. self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar)) self.start_turn(newchar) # Start the new character's turn. diff --git a/evennia/contrib/turnbattle/tb_equip.py b/evennia/contrib/turnbattle/tb_equip.py index 41fa93d4dd..087cc5b83f 100644 --- a/evennia/contrib/turnbattle/tb_equip.py +++ b/evennia/contrib/turnbattle/tb_equip.py @@ -60,10 +60,18 @@ from evennia.commands.default.help import CmdHelp """ ---------------------------------------------------------------------------- -COMBAT FUNCTIONS START HERE +OPTIONS ---------------------------------------------------------------------------- """ +TURN_TIMEOUT = 30 # Time before turns automatically end, in seconds +ACTIONS_PER_TURN = 1 # Number of actions allowed per turn + +""" +---------------------------------------------------------------------------- +COMBAT FUNCTIONS START HERE +---------------------------------------------------------------------------- +""" def roll_init(character): """ @@ -377,7 +385,7 @@ class TBEquipTurnHandler(DefaultScript): # Set up the current turn and turn timeout delay. self.db.turn = 0 - self.db.timer = 30 # 30 seconds + self.db.timer = TURN_TIMEOUT # Set timer to turn timeout specified in options def at_stop(self): """ @@ -431,7 +439,7 @@ class TBEquipTurnHandler(DefaultScript): separated for movement, by adding "character.db.combat_movesleft = 3" or something similar. """ - character.db.combat_actionsleft = 1 # 1 action per turn. + character.db.combat_actionsleft = ACTIONS_PER_TURN # Replenish actions # Prompt the character for their turn and give some information. character.msg("|wIt's your turn! You have %i HP remaining.|n" % character.db.hp) @@ -469,7 +477,7 @@ class TBEquipTurnHandler(DefaultScript): if self.db.turn > len(self.db.fighters) - 1: self.db.turn = 0 # Go back to the first in the turn order once you reach the end. newchar = self.db.fighters[self.db.turn] # Note the new character - self.db.timer = 30 + self.time_until_next_repeat() # Reset the timer. + self.db.timer = TURN_TIMEOUT + self.time_until_next_repeat() # Reset the timer. self.db.timeout_warning_given = False # Reset the timeout warning. self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar)) self.start_turn(newchar) # Start the new character's turn. diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index daeb4bad8f..43d9492f24 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -101,10 +101,18 @@ from evennia.commands.default.help import CmdHelp """ ---------------------------------------------------------------------------- -COMBAT FUNCTIONS START HERE +OPTIONS ---------------------------------------------------------------------------- """ +TURN_TIMEOUT = 30 # Time before turns automatically end, in seconds +ACTIONS_PER_TURN = 1 # Number of actions allowed per turn + +""" +---------------------------------------------------------------------------- +COMBAT FUNCTIONS START HERE +---------------------------------------------------------------------------- +""" def roll_init(character): """ @@ -548,7 +556,7 @@ class TBRangeTurnHandler(DefaultScript): # Set up the current turn and turn timeout delay. self.db.turn = 0 - self.db.timer = 30 # 30 seconds + self.db.timer = TURN_TIMEOUT # Set timer to turn timeout specified in options def at_stop(self): """ @@ -653,7 +661,7 @@ class TBRangeTurnHandler(DefaultScript): characters to both move and attack in the same turn (or, alternately, move twice or attack twice). """ - character.db.combat_actionsleft = 2 # 2 actions per turn. + character.db.combat_actionsleft = ACTIONS_PER_TURN # Replenish actions # Prompt the character for their turn and give some information. character.msg("|wIt's your turn!|n") combat_status_message(character) @@ -692,7 +700,7 @@ class TBRangeTurnHandler(DefaultScript): if self.db.turn > len(self.db.fighters) - 1: self.db.turn = 0 # Go back to the first in the turn order once you reach the end. newchar = self.db.fighters[self.db.turn] # Note the new character - self.db.timer = 30 + self.time_until_next_repeat() # Reset the timer. + self.db.timer = TURN_TIMEOUT + self.time_until_next_repeat() # Reset the timer. self.db.timeout_warning_given = False # Reset the timeout warning. self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar)) self.start_turn(newchar) # Start the new character's turn. From b2ec29db81864994a24f92e2aae22c9fe930d4dc Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 23 Oct 2017 21:51:03 -0700 Subject: [PATCH 30/68] distance_inc & distance_dec changed to helper funcs --- evennia/contrib/turnbattle/tb_range.py | 78 ++++++++++++++------------ 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index 43d9492f24..ef2859c677 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -78,6 +78,11 @@ And change your game's character typeclass to inherit from TBRangeCharacter instead of the default: class Character(TBRangeCharacter): + +Do the same thing in your game's objects.py module for TBRangeObject: + + from evennia.contrib.turnbattle.tb_range import TBRangeObject + class Object(TBRangeObject): Next, import this module into your default_cmdsets.py module: @@ -106,7 +111,7 @@ OPTIONS """ TURN_TIMEOUT = 30 # Time before turns automatically end, in seconds -ACTIONS_PER_TURN = 1 # Number of actions allowed per turn +ACTIONS_PER_TURN = 2 # Number of actions allowed per turn """ ---------------------------------------------------------------------------- @@ -282,41 +287,6 @@ def resolve_attack(attacker, defender, attack_type, attack_value=None, defense_v # If defender HP is reduced to 0 or less, call at_defeat. if defender.db.hp <= 0: at_defeat(defender) - -def distance_dec(mover, target): - """ - Decreases distance in range field between mover and target. - - Args: - mover (obj): The object moving - target (obj): The object to be moved toward - """ - mover.db.combat_range[target] -= 1 - target.db.combat_range[mover] = mover.db.combat_range[target] - # If this brings mover to range 0 (Engaged): - if mover.db.combat_range[target] <= 0: - # Reset range to each other to 0 and copy target's ranges to mover. - target.db.combat_range[mover] = 0 - mover.db.combat_range = target.db.combat_range - # Assure everything else has the same distance from the mover and target, now that they're together - for object in mover.location.contents: - if object != mover and object != target: - object.db.combat_range[mover] = object.db.combat_range[target] - -def distance_inc(mover, target): - """ - Increases distance in range field between mover and target. - - Args: - mover (obj): The object moving - target (obj): The object to be moved away from - """ - mover.db.combat_range[target] += 1 - target.db.combat_range[mover] = mover.db.combat_range[target] - # Set a cap of 2: - if mover.db.combat_range[target] > 2: - target.db.combat_range[mover] = 2 - mover.db.combat_range[target] = 2 def approach(mover, target): """ @@ -331,6 +301,26 @@ def approach(mover, target): target than the mover is. The mover will also move away from anything they started out close to. """ + def distance_dec(mover, target): + """ + Helper function that decreases distance in range field between mover and target. + + Args: + mover (obj): The object moving + target (obj): The object to be moved toward + """ + mover.db.combat_range[target] -= 1 + target.db.combat_range[mover] = mover.db.combat_range[target] + # If this brings mover to range 0 (Engaged): + if mover.db.combat_range[target] <= 0: + # Reset range to each other to 0 and copy target's ranges to mover. + target.db.combat_range[mover] = 0 + mover.db.combat_range = target.db.combat_range + # Assure everything else has the same distance from the mover and target, now that they're together + for object in mover.location.contents: + if object != mover and object != target: + object.db.combat_range[mover] = object.db.combat_range[target] + objects = mover.location.contents for thing in objects: @@ -358,7 +348,23 @@ def withdraw(mover, target): of their withdrawl. The mover will never inadvertently move toward anything else while withdrawing - they can be considered to be moving to open space. """ + def distance_inc(mover, target): + """ + Helper function that increases distance in range field between mover and target. + + Args: + mover (obj): The object moving + target (obj): The object to be moved away from + """ + mover.db.combat_range[target] += 1 + target.db.combat_range[mover] = mover.db.combat_range[target] + # Set a cap of 2: + if mover.db.combat_range[target] > 2: + target.db.combat_range[mover] = 2 + mover.db.combat_range[target] = 2 + objects = mover.location.contents + for thing in objects: if thing != mover and thing != target: # Move away from each object closer to the target than you, if it's also closer to you than you are to the target. From e742179310142ce8850273fb0b2ad5c0d2fdf9eb Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 23 Oct 2017 21:57:00 -0700 Subject: [PATCH 31/68] References to 'object' changed to 'thing' instead --- evennia/contrib/turnbattle/tb_basic.py | 6 +-- evennia/contrib/turnbattle/tb_equip.py | 6 +-- evennia/contrib/turnbattle/tb_range.py | 70 +++++++++++++------------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_basic.py b/evennia/contrib/turnbattle/tb_basic.py index 017094d0ec..88e5c176c8 100644 --- a/evennia/contrib/turnbattle/tb_basic.py +++ b/evennia/contrib/turnbattle/tb_basic.py @@ -370,9 +370,9 @@ class TBBasicTurnHandler(DefaultScript): self.db.fighters = [] # Add all fighters in the room with at least 1 HP to the combat." - for object in self.obj.contents: - if object.db.hp: - self.db.fighters.append(object) + for thing in self.obj.contents: + if thing.db.hp: + self.db.fighters.append(thing) # Initialize each fighter for combat for fighter in self.db.fighters: diff --git a/evennia/contrib/turnbattle/tb_equip.py b/evennia/contrib/turnbattle/tb_equip.py index 087cc5b83f..b6b7ae6035 100644 --- a/evennia/contrib/turnbattle/tb_equip.py +++ b/evennia/contrib/turnbattle/tb_equip.py @@ -361,9 +361,9 @@ class TBEquipTurnHandler(DefaultScript): self.db.fighters = [] # Add all fighters in the room with at least 1 HP to the combat." - for object in self.obj.contents: - if object.db.hp: - self.db.fighters.append(object) + for thing in self.obj.contents: + if thing.db.hp: + self.db.fighters.append(thing) # Initialize each fighter for combat for fighter in self.db.fighters: diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index ef2859c677..bfec0893f0 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -317,13 +317,13 @@ def approach(mover, target): target.db.combat_range[mover] = 0 mover.db.combat_range = target.db.combat_range # Assure everything else has the same distance from the mover and target, now that they're together - for object in mover.location.contents: - if object != mover and object != target: - object.db.combat_range[mover] = object.db.combat_range[target] + for thing in mover.location.contents: + if thing != mover and thing != target: + thing.db.combat_range[mover] = thing.db.combat_range[target] - objects = mover.location.contents + contents = mover.location.contents - for thing in objects: + for thing in contents: if thing != mover and thing != target: # Move closer to each object closer to the target than you. if mover.db.combat_range[thing] > target.db.combat_range[thing]: @@ -363,9 +363,9 @@ def withdraw(mover, target): target.db.combat_range[mover] = 2 mover.db.combat_range[target] = 2 - objects = mover.location.contents + contents = mover.location.contents - for thing in objects: + for thing in contents: if thing != mover and thing != target: # Move away from each object closer to the target than you, if it's also closer to you than you are to the target. if mover.db.combat_range[thing] >= target.db.combat_range[thing] and mover.db.combat_range[thing] < mover.db.combat_range[thing]: @@ -486,14 +486,14 @@ def combat_status_message(fighter): reach_obj = [] range_obj = [] - for object in fighter.db.combat_range: - if object != fighter: - if fighter.db.combat_range[object] == 0: - engaged_obj.append(object) - if fighter.db.combat_range[object] == 1: - reach_obj.append(object) - if fighter.db.combat_range[object] > 1: - range_obj.append(object) + for thing in fighter.db.combat_range: + if thing != fighter: + if fighter.db.combat_range[thing] == 0: + engaged_obj.append(thing) + if fighter.db.combat_range[thing] == 1: + reach_obj.append(thing) + if fighter.db.combat_range[thing] > 1: + range_obj.append(thing) if engaged_obj: status_msg += "|/Engaged targets: %s" % ", ".join(obj.key for obj in engaged_obj) @@ -534,9 +534,9 @@ class TBRangeTurnHandler(DefaultScript): self.db.fighters = [] # Add all fighters in the room with at least 1 HP to the combat." - for object in self.obj.contents: - if object.db.hp: - self.db.fighters.append(object) + for thing in self.obj.contents: + if thing.db.hp: + self.db.fighters.append(thing) # Initialize each fighter for combat for fighter in self.db.fighters: @@ -546,8 +546,8 @@ class TBRangeTurnHandler(DefaultScript): self.obj.db.combat_turnhandler = self # Initialize range field for all objects in the room - for object in self.obj.contents: - self.init_range(object) + for thing in self.obj.contents: + self.init_range(thing) # Roll initiative and sort the list of fighters depending on who rolls highest to determine turn order. # The initiative roll is determined by the roll_init function and can be customized easily. @@ -568,8 +568,8 @@ class TBRangeTurnHandler(DefaultScript): """ Called at script termination. """ - for object in self.obj.contents: - combat_cleanup(object) # Clean up the combat attributes for every object in the room. + for thing in self.obj.contents: + combat_cleanup(thing) # Clean up the combat attributes for every object in the room. self.obj.db.combat_turnhandler = None # Remove reference to turn handler in location def at_repeat(self): @@ -599,17 +599,17 @@ class TBRangeTurnHandler(DefaultScript): rangedict = {} # Get a list of objects in the room. objectlist = self.obj.contents - for object in objectlist: + for thing in objectlist: # Object always at distance 0 from itself - if object == to_init: - rangedict.update({object:0}) + if thing == to_init: + rangedict.update({thing:0}) else: - if object.destination or to_init.destination: + if thing.destination or to_init.destination: # Start exits at range 2 to put them at the 'edges' - rangedict.update({object:2}) + rangedict.update({thing:2}) else: # Start objects at range 1 from other objects - rangedict.update({object:1}) + rangedict.update({thing:1}) to_init.db.combat_range = rangedict def join_rangefield(self, to_init, anchor_obj=None, add_distance=0): @@ -633,9 +633,9 @@ class TBRangeTurnHandler(DefaultScript): # Copy the range values from the anchor object. to_init.db.combat_range = anchor_obj.db.combat_range # Add the new object to everyone else's ranges. - for object in contents: - new_objects_range = object.db.combat_range[anchor_obj] - object.db.combat_range.update({to_init:new_objects_range}) + for thing in contents: + new_objects_range = thing.db.combat_range[anchor_obj] + thing.db.combat_range.update({to_init:new_objects_range}) # Set the new object's range to itself to 0. to_init.db.combat_range.update({to_init:0}) # Add additional distance from anchor object, if any. @@ -888,10 +888,10 @@ class TBRangeObject(DefaultObject): if self.db.combat_range: del self.db.combat_range # Remove this object from everyone's range fields - for object in getter.location.contents: - if object.db.combat_range: - if self in object.db.combat_range: - object.db.combat_range.pop(self, None) + for thing in getter.location.contents: + if thing.db.combat_range: + if self in thing.db.combat_range: + thing.db.combat_range.pop(self, None) # If in combat, getter spends an action if is_in_combat(getter): spend_action(getter, 1, action_name="get") # Use up one action. From 66bb313c34e528aeb5a7991e89b3d3c63bd570a8 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 23 Oct 2017 21:59:01 -0700 Subject: [PATCH 32/68] Empty returns deleted, methods properly spaced --- evennia/contrib/turnbattle/tb_range.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index bfec0893f0..143134beec 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -333,7 +333,6 @@ def approach(mover, target): distance_inc(mover, thing) # Lastly, move closer to your target. distance_dec(mover, target) - return def withdraw(mover, target): """ @@ -378,7 +377,6 @@ def withdraw(mover, target): distance_inc(mover, thing) # Then, move away from your target. distance_inc(mover, target) - return def get_range(obj1, obj2): """ @@ -503,7 +501,6 @@ def combat_status_message(fighter): status_msg += "|/Ranged targets: %s" % ", ".join(obj.key for obj in range_obj) fighter.msg(status_msg) - return """ ---------------------------------------------------------------------------- @@ -823,6 +820,7 @@ class TBRangeObject(DefaultObject): dropper.msg("You can only drop things on your turn!") return False return True + def at_drop(self, dropper): """ Called by the default `drop` command when this object has been @@ -843,6 +841,7 @@ class TBRangeObject(DefaultObject): # Object joins the range field self.db.combat_range = {} dropper.location.db.combat_turnhandler.join_rangefield(self, anchor_obj=dropper) + def at_before_get(self, getter): """ Called by the default `get` command before this object has been @@ -869,6 +868,7 @@ class TBRangeObject(DefaultObject): getter.msg("You aren't close enough to get that! (see: help approach)") return False return True + def at_get(self, getter): """ Called by the default `get` command when this object has been @@ -895,6 +895,7 @@ class TBRangeObject(DefaultObject): # If in combat, getter spends an action if is_in_combat(getter): spend_action(getter, 1, action_name="get") # Use up one action. + def at_before_give(self, giver, getter): """ Called by the default `give` command before this object has been @@ -923,6 +924,7 @@ class TBRangeObject(DefaultObject): giver.msg("You aren't close enough to give things to %s! (see: help approach)" % getter) return False return True + def at_give(self, giver, getter): """ Called by the default `give` command when this object has been From 95f840ac7abc6bbfad8eb37bbbc72f654c5db783 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 23 Oct 2017 22:05:56 -0700 Subject: [PATCH 33/68] get_range integrated into movement functions This makes the code a bit more readable and fixes a bug in withdrawing that didn't take other objects into account properly. --- evennia/contrib/turnbattle/tb_range.py | 60 +++++++++++++------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index 143134beec..d848d445b7 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -287,6 +287,29 @@ def resolve_attack(attacker, defender, attack_type, attack_value=None, defense_v # If defender HP is reduced to 0 or less, call at_defeat. if defender.db.hp <= 0: at_defeat(defender) + +def get_range(obj1, obj2): + """ + Gets the combat range between two objects. + + Args: + obj1 (obj): First object + obj2 (obj): Second object + + Returns: + range (int or None): Distance between two objects or None if not applicable + """ + # Return None if not applicable. + if not obj1.db.combat_range: + return None + if not obj2.db.combat_range: + return None + if obj1 not in obj2.db.combat_range: + return None + if obj2 not in obj1.db.combat_range: + return None + # Return the range between the two objects. + return obj1.db.combat_range[obj2] def approach(mover, target): """ @@ -312,7 +335,7 @@ def approach(mover, target): mover.db.combat_range[target] -= 1 target.db.combat_range[mover] = mover.db.combat_range[target] # If this brings mover to range 0 (Engaged): - if mover.db.combat_range[target] <= 0: + if get_range(mover, target) <= 0: # Reset range to each other to 0 and copy target's ranges to mover. target.db.combat_range[mover] = 0 mover.db.combat_range = target.db.combat_range @@ -326,10 +349,10 @@ def approach(mover, target): for thing in contents: if thing != mover and thing != target: # Move closer to each object closer to the target than you. - if mover.db.combat_range[thing] > target.db.combat_range[thing]: + if get_range(mover, thing) > get_range(target, thing): distance_dec(mover, thing) # Move further from each object that's further from you than from the target. - if mover.db.combat_range[thing] < target.db.combat_range[thing]: + if get_range(mover, thing) < get_range(target, thing): distance_inc(mover, thing) # Lastly, move closer to your target. distance_dec(mover, target) @@ -358,7 +381,7 @@ def withdraw(mover, target): mover.db.combat_range[target] += 1 target.db.combat_range[mover] = mover.db.combat_range[target] # Set a cap of 2: - if mover.db.combat_range[target] > 2: + if get_range(mover, target) > 2: target.db.combat_range[mover] = 2 mover.db.combat_range[target] = 2 @@ -367,39 +390,16 @@ def withdraw(mover, target): for thing in contents: if thing != mover and thing != target: # Move away from each object closer to the target than you, if it's also closer to you than you are to the target. - if mover.db.combat_range[thing] >= target.db.combat_range[thing] and mover.db.combat_range[thing] < mover.db.combat_range[thing]: + if get_range(mover, thing) >= get_range(target, thing) and get_range(mover, thing) < get_range(mover, target): distance_inc(mover, thing) # Move away from anything your target is engaged with - if target.db.combat_range[thing] == 0: + if get_range(target, thing) == 0: distance_inc(mover, thing) # Move away from anything you're engaged with. - if mover.db.combat_range[thing] == 0: + if get_range(mover, thing) == 0: distance_inc(mover, thing) # Then, move away from your target. distance_inc(mover, target) - -def get_range(obj1, obj2): - """ - Gets the combat range between two objects. - - Args: - obj1 (obj): First object - obj2 (obj): Second object - - Returns: - range (int or None): Distance between two objects or None if not applicable - """ - # Return None if not applicable. - if not obj1.db.combat_range: - return None - if not obj2.db.combat_range: - return None - if obj1 not in obj2.db.combat_range: - return None - if obj2 not in obj1.db.combat_range: - return None - # Return the range between the two objects. - return obj1.db.combat_range[obj2] def combat_cleanup(character): """ From 1fe9bf3dceb09658bd25c3a9a6283003a8506f22 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 23 Oct 2017 22:29:45 -0700 Subject: [PATCH 34/68] Moved distance_inc back to being its own function I almost forgot - distance_inc is actually used by both 'approach' and 'withdraw', since approaching an object might put you farther away from others. So, I moved it back to its own function. --- evennia/contrib/turnbattle/tb_range.py | 29 +++++++++++++------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index d848d445b7..0eb617c48b 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -310,6 +310,21 @@ def get_range(obj1, obj2): return None # Return the range between the two objects. return obj1.db.combat_range[obj2] + +def distance_inc(mover, target): + """ + Function that increases distance in range field between mover and target. + + Args: + mover (obj): The object moving + target (obj): The object to be moved away from + """ + mover.db.combat_range[target] += 1 + target.db.combat_range[mover] = mover.db.combat_range[target] + # Set a cap of 2: + if get_range(mover, target) > 2: + target.db.combat_range[mover] = 2 + mover.db.combat_range[target] = 2 def approach(mover, target): """ @@ -370,20 +385,6 @@ def withdraw(mover, target): of their withdrawl. The mover will never inadvertently move toward anything else while withdrawing - they can be considered to be moving to open space. """ - def distance_inc(mover, target): - """ - Helper function that increases distance in range field between mover and target. - - Args: - mover (obj): The object moving - target (obj): The object to be moved away from - """ - mover.db.combat_range[target] += 1 - target.db.combat_range[mover] = mover.db.combat_range[target] - # Set a cap of 2: - if get_range(mover, target) > 2: - target.db.combat_range[mover] = 2 - mover.db.combat_range[target] = 2 contents = mover.location.contents From 2475d14691baf707a051283932e8c93bcd85536f Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 28 Oct 2017 00:13:40 +0200 Subject: [PATCH 35/68] Almost finished with kwargs-support for evmenu --- evennia/utils/evmenu.py | 250 ++++++++++++++++++++++++++-------------- 1 file changed, 166 insertions(+), 84 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index d248910e76..cde028cf50 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -63,23 +63,35 @@ menu is immediately exited and the default "look" command is called. text (str, tuple or None): Text shown at this node. If a tuple, the second element in the tuple is a help text to display at this node when the user enters the menu help command there. - options (tuple, dict or None): ( - {'key': name, # can also be a list of aliases. A special key is - # "_default", which marks this option as the default - # fallback when no other option matches the user input. - 'desc': description, # optional description - 'goto': nodekey, # node to go to when chosen. This can also be a callable with - # caller and/or raw_string args. It must return a string - # with the key pointing to the node to go to. - 'exec': nodekey}, # node or callback to trigger as callback when chosen. This - # will execute *before* going to the next node. Both node - # and the explicit callback will be called as normal nodes - # (with caller and/or raw_string args). If the callable/node - # returns a single string (only), this will replace the current - # goto location string in-place (if a goto callback, it will never fire). - # Note that relying to much on letting exec assign the goto - # location can make it hard to debug your menu logic. - {...}, ...) + options (tuple, dict or None): If `None`, this exits the menu. + If a single dict, this is a single-option node. If a tuple, + it should be a tuple of option dictionaries. Option dicts have + the following keys: + - `key` (str or tuple, optional): What to enter to choose this option. + If a tuple, it must be a tuple of strings, where the first string is the + key which will be shown to the user and the others are aliases. + If unset, the options' number will be used. The special key `_default` + marks this option as the default fallback when no other option matches + the user input. There can only be one `_default` option per node. It + will not be displayed in the list. + - `desc` (str, optional): This describes what choosing the option will do. + - `goto` (str, tuple or callable): If string, should be the name of node to go to + when this option is selected. If a callable, it has the signature + `callable(caller[,raw_input][,**kwargs]). If a tuple, the first element + is the callable and the second is a dict with the **kwargs to pass to + the callable. Those kwargs will also be passed into the next node if possible. + Such a callable should return either a str or a (str, dict), where the + string is the name of the next node to go to and the dict is the new, + (possibly modified) kwarg to pass into the next node. + - `exec` (str, callable or tuple, optional): This specified either the name of + a menu node to execute as a callback or a regular callable. If a tuple, the + first element is either the menu-node name or the callback, while the second + is a dict for the **kwargs to pass into the node/callback. This callback/node + will execute *before* going any `goto` function and before going to the next + node. The callback should look like a node, so `callback(caller[,raw_input][,**kwargs])`. + If this callable returns a single string (only) then that will replace the + current goto location (if a `goto` callback is set, it will never fire). Returning + anything else has no effect. If key is not given, the option will automatically be identified by its number 1..N. @@ -519,7 +531,43 @@ class EvMenu(object): # format the entire node return self.node_formatter(nodetext, optionstext) - def _execute_node(self, nodename, raw_string): + def _safe_call(self, callback, raw_string, **kwargs): + """ + Call a node-like callable, with a variable number of raw_string, *args, **kwargs, all of + which should work also if not present (only `caller` is always required). Return its result. + + """ + try: + nspec = getargspec(callback).args + kspec = getargspec(callback).defaults + try: + # this counts both args and kwargs + nspec = len(nspec) + except TypeError: + raise EvMenuError("Callable {} doesn't accept any arguments!".format(callback)) + nkwargs = len(kspec) if kspec else 0 + nargs = nspec - nkwargs + if nargs <= 0: + raise EvMenuError("Callable {} doesn't accept any arguments!".format(callback)) + + if nkwargs: + if nargs > 1: + return callback(self.caller, raw_string, **kwargs) + # callback accepting raw_string, **kwargs + else: + # callback accepting **kwargs + return callback(self.caller, **kwargs) + elif nargs > 1: + # callback accepting raw_string + return callback(self.caller, raw_string) + else: + # normal callback, only the caller as arg + return callback(self.caller) + except Exception: + self.caller.msg(_ERR_GENERAL.format(nodename=callback), self._session) + raise + + def _execute_node(self, nodename, raw_string, **kwargs): """ Execute a node. @@ -528,6 +576,7 @@ class EvMenu(object): raw_string (str): The raw default string entered on the previous node (only used if the node accepts it as an argument) + kwargs (any, optional): Optional kwargs for the node. Returns: nodetext, options (tuple): The node text (a string or a @@ -540,13 +589,7 @@ class EvMenu(object): self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session) raise EvMenuError try: - # the node should return data as (text, options) - if len(getargspec(node).args) > 1: - # a node accepting raw_string - nodetext, options = node(self.caller, raw_string) - else: - # a normal node, only accepting caller - nodetext, options = node(self.caller) + nodetext, options = self._safe_call(node, raw_string, **kwargs) except KeyError: self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session) raise EvMenuError @@ -555,32 +598,7 @@ class EvMenu(object): raise return nodetext, options - def display_nodetext(self): - self.caller.msg(self.nodetext, session=self._session) - - def display_helptext(self): - self.caller.msg(self.helptext, session=self._session) - - def callback_goto(self, callback, goto, raw_string): - """ - Call callback and goto in sequence. - - Args: - callback (callable or str): Callback to run before goto. If - the callback returns a string, this is used to replace - the `goto` string before going to the next node. - goto (str): The target node to go to next (unless replaced - by `callable`).. - raw_string (str): The original user input. - - """ - if callback: - # replace goto only if callback returns - goto = self.callback(callback, raw_string) or goto - if goto: - self.goto(goto, raw_string) - - def callback(self, nodename, raw_string): + def run_exec(self, nodename, raw_string, **kwargs): """ Run a function or node as a callback (with the 'exec' option key). @@ -592,6 +610,8 @@ class EvMenu(object): raw_string (str): The raw default string entered on the previous node (only used if the node accepts it as an argument) + kwargs (any): These are optional kwargs passed into goto + Returns: new_goto (str or None): A replacement goto location string or None (no replacement). @@ -604,34 +624,30 @@ class EvMenu(object): """ if callable(nodename): # this is a direct callable - execute it directly - try: - if len(getargspec(nodename).args) > 1: - # callable accepting raw_string - ret = nodename(self.caller, raw_string) - else: - # normal callable, only the caller as arg - ret = nodename(self.caller) - except Exception: - self.caller.msg(_ERR_GENERAL.format(nodename=nodename), self._session) - raise + ret = self._safe_call(nodename, raw_string, **kwargs) else: - # nodename is a string; lookup as node + # nodename is a string; lookup as node and run as node (but don't + # care about options) try: # execute the node - ret = self._execute_node(nodename, raw_string) + ret = self._execute_node(nodename, raw_string, **kwargs) + if isinstance(ret, (tuple, list)) and len(ret) == 2: + # a (text, options) tuple. We only want the text. + ret = ret[0] except EvMenuError as err: errmsg = "Error in exec '%s' (input: '%s'): %s" % (nodename, raw_string, err) self.caller.msg("|r%s|n" % errmsg) logger.log_trace(errmsg) return + if isinstance(ret, basestring): # only return a value if a string (a goto target), ignore all other returns return ret return None - def goto(self, nodename, raw_string): + def goto(self, nodename, raw_string, **kwargs): """ - Run a node by name + Run a node by name, optionally dynamically generating that name first. Args: nodename (str or callable): Name of node or a callable @@ -642,19 +658,40 @@ class EvMenu(object): argument) """ - if callable(nodename): - try: - if len(getargspec(nodename).args) > 1: - # callable accepting raw_string - nodename = nodename(self.caller, raw_string) + def _extract_goto_exec(option_dict): + "Helper: Get callables and their eventual kwargs" + goto_kwargs, exec_kwargs = {}, {} + goto, execute = option_dict.get("goto", None), option_dict.get("exec", None) + if goto and isinstance(goto, (tuple, list)): + if len(goto) > 1: + goto, goto_kwargs = goto[:2] # ignore any extra arguments + if not hasattr(goto_kwargs, "__getitem__"): + # not a dict-like structure + raise EvMenuError("EvMenu node {}: goto kwargs is not a dict: {}".format( + nodename, goto_kwargs)) else: - nodename = nodename(self.caller) - except Exception: - self.caller.msg(_ERR_GENERAL.format(nodename=nodename), self._session) - raise + goto = goto[0] + if execute and isinstance(execute, (tuple, list)): + if len(execute) > 1: + execute, exec_kwargs = execute[:2] # ignore any extra arguments + if not hasattr(exec_kwargs, "__getitem__"): + # not a dict-like structure + raise EvMenuError("EvMenu node {}: exec kwargs is not a dict: {}".format( + nodename, goto_kwargs)) + else: + execute = execute[0] + return goto, goto_kwargs, execute, exec_kwargs + + if callable(nodename): + # run the "goto" callable, if possible + nodename = self._safe_call(nodename, raw_string, **kwargs) + if isinstance(nodename, (tuple, list)): + if not len(nodename) > 1 or not isinstance(nodename[1], dict): + raise EvMenuError("{}: goto callable must return str or (str, dict)") + nodename, kwargs = nodename[:2] try: - # execute the node, make use of the returns. - nodetext, options = self._execute_node(nodename, raw_string) + # execute the found node, make use of the returns. + nodetext, options = self._execute_node(nodename, raw_string, **kwargs) except EvMenuError: return @@ -683,17 +720,19 @@ class EvMenu(object): if "_default" in keys: keys = [key for key in keys if key != "_default"] desc = dic.get("desc", dic.get("text", _ERR_NO_OPTION_DESC).strip()) - goto, execute = dic.get("goto", None), dic.get("exec", None) - self.default = (goto, execute) + goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic) + self.default = (goto, goto_kwargs, execute, exec_kwargs) else: + # use the key (only) if set, otherwise use the running number keys = list(make_iter(dic.get("key", str(inum + 1).strip()))) desc = dic.get("desc", dic.get("text", _ERR_NO_OPTION_DESC).strip()) - goto, execute = dic.get("goto", None), dic.get("exec", None) + goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic) if keys: display_options.append((keys[0], desc)) for key in keys: if goto or execute: - self.options[strip_ansi(key).strip().lower()] = (goto, execute) + self.options[strip_ansi(key).strip().lower()] = \ + (goto, goto_kwargs, execute, exec_kwargs) self.nodetext = self._format_node(nodetext, display_options) @@ -709,6 +748,28 @@ class EvMenu(object): if not options: self.close_menu() + def run_exec_then_goto(self, runexec, goto, raw_string, runexec_kwargs=None, goto_kwargs=None): + """ + Call 'exec' callback and goto (which may also be a callable) in sequence. + + Args: + runexec (callable or str): Callback to run before goto. If + the callback returns a string, this is used to replace + the `goto` string/callable before being passed into the goto handler. + goto (str): The target node to go to next (may be replaced + by `runexec`).. + raw_string (str): The original user input. + runexec_kwargs (dict, optional): Optional kwargs for runexec. + goto_kwargs (dict, optional): Optional kwargs for goto. + + """ + if runexec: + # replace goto only if callback returns + goto = self.run_exec(runexec, raw_string, + **(runexec_kwargs if runexec_kwargs else {})) or goto + if goto: + self.goto(goto, raw_string, **(goto_kwargs if goto_kwargs else {})) + def close_menu(self): """ Shutdown menu; occurs when reaching the end node or using the quit command. @@ -739,8 +800,8 @@ class EvMenu(object): if cmd in self.options: # this will take precedence over the default commands # below - goto, callback = self.options[cmd] - self.callback_goto(callback, goto, raw_string) + goto, goto_kwargs, execfunc, exec_kwargs = self.options[cmd] + self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs) elif self.auto_look and cmd in ("look", "l"): self.display_nodetext() elif self.auto_help and cmd in ("help", "h"): @@ -748,8 +809,8 @@ class EvMenu(object): elif self.auto_quit and cmd in ("quit", "q", "exit"): self.close_menu() elif self.default: - goto, callback = self.default - self.callback_goto(callback, goto, raw_string) + goto, goto_kwargs, execfunc, exec_kwargs = self.default + self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs) else: self.caller.msg(_HELP_NO_OPTION_MATCH, session=self._session) @@ -757,6 +818,12 @@ class EvMenu(object): # no options - we are at the end of the menu. self.close_menu() + def display_nodetext(self): + self.caller.msg(self.nodetext, session=self._session) + + def display_helptext(self): + self.caller.msg(self.helptext, session=self._session) + # formatters - override in a child class def nodetext_formatter(self, nodetext): @@ -996,6 +1063,10 @@ def get_input(caller, prompt, callback, session=None, *args, **kwargs): # # ------------------------------------------------------------- +def _generate_goto(caller, **kwargs): + return kwargs.get("name", "text_start_node"), {"name": "replaced!"} + + def test_start_node(caller): menu = caller.ndb._menutree text = """ @@ -1020,6 +1091,9 @@ def test_start_node(caller): {"key": ("|yV|niew", "v"), "desc": "View your own name", "goto": "test_view_node"}, + {"key": ("|yD|nynamic", "d"), + "desc": "Dynamic node", + "goto": (_generate_goto, {"name": "test_dynamic_node"})}, {"key": ("|yQ|nuit", "quit", "q", "Q"), "desc": "Quit this menu example.", "goto": "test_end_node"}, @@ -1095,6 +1169,14 @@ def test_displayinput_node(caller, raw_string): return text, options +def test_dynamic_node(caller, **kwargs): + text = """ + This is a dynamic node, whose name was + generated by the goto function. + """ + options = {} + return text, options + def test_end_node(caller): text = """ This is the end of the menu and since it has no options the menu From 7b295fa98b37a3812b533d77d76999c4e7b3a9b9 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 28 Oct 2017 12:05:32 +0200 Subject: [PATCH 36/68] Add working **kwargs support to nodes/callbacks in evmenu --- evennia/utils/evmenu.py | 172 +++++++++++++++++++++++++--------------- 1 file changed, 109 insertions(+), 63 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index cde028cf50..6f31822bf3 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -83,15 +83,11 @@ menu is immediately exited and the default "look" command is called. Such a callable should return either a str or a (str, dict), where the string is the name of the next node to go to and the dict is the new, (possibly modified) kwarg to pass into the next node. - - `exec` (str, callable or tuple, optional): This specified either the name of - a menu node to execute as a callback or a regular callable. If a tuple, the - first element is either the menu-node name or the callback, while the second - is a dict for the **kwargs to pass into the node/callback. This callback/node - will execute *before* going any `goto` function and before going to the next - node. The callback should look like a node, so `callback(caller[,raw_input][,**kwargs])`. - If this callable returns a single string (only) then that will replace the - current goto location (if a `goto` callback is set, it will never fire). Returning - anything else has no effect. + - `exec` (str, callable or tuple, optional): This takes the same input as `goto` above + and runs before it. If given a node name, the node will be executed but will not + be considered the next node. If node/callback returns str or (str, dict), these will + replace the `goto` step (`goto` callbacks will not fire), with the string being the + next node name and the optional dict acting as the kwargs-input for the next node. If key is not given, the option will automatically be identified by its number 1..N. @@ -107,7 +103,7 @@ Example: "This is help text for this node") options = ({"key": "testing", "desc": "Select this to go to node 2", - "goto": "node2", + "goto": ("node2", {"foo": "bar"}), "exec": "callback1"}, {"desc": "Go to node 3.", "goto": "node3"}) @@ -120,12 +116,13 @@ Example: # by the normal 'goto' option key above. caller.msg("Callback called!") - def node2(caller): + def node2(caller, **kwargs): text = ''' This is node 2. It only allows you to go back to the original node1. This extra indent will - be stripped. We don't include a help text. - ''' + be stripped. We don't include a help text but + here are the variables passed to us: {} + '''.format(kwargs) options = {"goto": "node1"} return text, options @@ -160,6 +157,7 @@ evennia.utils.evmenu`. """ from __future__ import print_function +import random from builtins import object, range from textwrap import dedent @@ -403,6 +401,7 @@ class EvMenu(object): self._startnode = startnode self._menutree = self._parse_menudata(menudata) self._persistent = persistent + self._quitting = False if startnode not in self._menutree: raise EvMenuError("Start node '%s' not in menu tree!" % startnode) @@ -538,35 +537,34 @@ class EvMenu(object): """ try: - nspec = getargspec(callback).args - kspec = getargspec(callback).defaults try: - # this counts both args and kwargs - nspec = len(nspec) + nargs = len(getargspec(callback).args) except TypeError: raise EvMenuError("Callable {} doesn't accept any arguments!".format(callback)) - nkwargs = len(kspec) if kspec else 0 - nargs = nspec - nkwargs + supports_kwargs = bool(getargspec(callback).keywords) if nargs <= 0: raise EvMenuError("Callable {} doesn't accept any arguments!".format(callback)) - if nkwargs: + if supports_kwargs: if nargs > 1: - return callback(self.caller, raw_string, **kwargs) + ret = callback(self.caller, raw_string, **kwargs) # callback accepting raw_string, **kwargs else: # callback accepting **kwargs - return callback(self.caller, **kwargs) + ret = callback(self.caller, **kwargs) elif nargs > 1: # callback accepting raw_string - return callback(self.caller, raw_string) + ret = callback(self.caller, raw_string) else: # normal callback, only the caller as arg - return callback(self.caller) - except Exception: - self.caller.msg(_ERR_GENERAL.format(nodename=callback), self._session) + ret = callback(self.caller) + except EvMenuError: + errmsg = _ERR_GENERAL.format(nodename=callback) + self.caller.msg(errmsg, self._session) raise + return ret + def _execute_node(self, nodename, raw_string, **kwargs): """ Execute a node. @@ -589,7 +587,11 @@ class EvMenu(object): self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session) raise EvMenuError try: - nodetext, options = self._safe_call(node, raw_string, **kwargs) + ret = self._safe_call(node, raw_string, **kwargs) + if isinstance(ret, (tuple, list)) and len(ret) > 1: + nodetext, options = ret[:2] + else: + nodetext, options = ret, None except KeyError: self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session) raise EvMenuError @@ -622,27 +624,31 @@ class EvMenu(object): relying on this. """ - if callable(nodename): - # this is a direct callable - execute it directly - ret = self._safe_call(nodename, raw_string, **kwargs) - else: - # nodename is a string; lookup as node and run as node (but don't - # care about options) - try: + try: + if callable(nodename): + # this is a direct callable - execute it directly + ret = self._safe_call(nodename, raw_string, **kwargs) + if isinstance(ret, (tuple, list)): + if not len(ret) > 1 or not isinstance(ret[1], dict): + raise EvMenuError("exec callable must return either None, str or (str, dict)") + ret, kwargs = ret[:2] + else: + # nodename is a string; lookup as node and run as node in-place (don't goto it) # execute the node ret = self._execute_node(nodename, raw_string, **kwargs) - if isinstance(ret, (tuple, list)) and len(ret) == 2: - # a (text, options) tuple. We only want the text. - ret = ret[0] - except EvMenuError as err: - errmsg = "Error in exec '%s' (input: '%s'): %s" % (nodename, raw_string, err) - self.caller.msg("|r%s|n" % errmsg) - logger.log_trace(errmsg) - return + if isinstance(ret, (tuple, list)): + if not len(ret) > 1 and ret[1] and not isinstance(ret[1], dict): + raise EvMenuError("exec node must return either None, str or (str, dict)") + ret, kwargs = ret[:2] + except EvMenuError as err: + errmsg = "Error in exec '%s' (input: '%s'): %s" % (nodename, raw_string.rstrip(), err) + self.caller.msg("|r%s|n" % errmsg) + logger.log_trace(errmsg) + return if isinstance(ret, basestring): # only return a value if a string (a goto target), ignore all other returns - return ret + return ret, kwargs return None def goto(self, nodename, raw_string, **kwargs): @@ -684,10 +690,12 @@ class EvMenu(object): if callable(nodename): # run the "goto" callable, if possible + inp_nodename = nodename nodename = self._safe_call(nodename, raw_string, **kwargs) if isinstance(nodename, (tuple, list)): if not len(nodename) > 1 or not isinstance(nodename[1], dict): - raise EvMenuError("{}: goto callable must return str or (str, dict)") + raise EvMenuError( + "{}: goto callable must return str or (str, dict)".format(inp_nodename)) nodename, kwargs = nodename[:2] try: # execute the found node, make use of the returns. @@ -765,8 +773,10 @@ class EvMenu(object): """ if runexec: # replace goto only if callback returns - goto = self.run_exec(runexec, raw_string, - **(runexec_kwargs if runexec_kwargs else {})) or goto + goto, goto_kwargs = ( + self.run_exec(runexec, raw_string, + **(runexec_kwargs if runexec_kwargs else {})) or + (goto, goto_kwargs)) if goto: self.goto(goto, raw_string, **(goto_kwargs if goto_kwargs else {})) @@ -774,13 +784,16 @@ class EvMenu(object): """ Shutdown menu; occurs when reaching the end node or using the quit command. """ - self.caller.cmdset.remove(EvMenuCmdSet) - del self.caller.ndb._menutree - if self._persistent: - self.caller.attributes.remove("_menutree_saved") - self.caller.attributes.remove("_menutree_saved_startnode") - if self.cmd_on_exit is not None: - self.cmd_on_exit(self.caller, self) + if not self._quitting: + # avoid multiple calls from different sources + self._quitting = True + self.caller.cmdset.remove(EvMenuCmdSet) + del self.caller.ndb._menutree + if self._persistent: + self.caller.attributes.remove("_menutree_saved") + self.caller.attributes.remove("_menutree_saved_startnode") + if self.cmd_on_exit is not None: + self.cmd_on_exit(self.caller, self) def parse_input(self, raw_string): """ @@ -1064,7 +1077,7 @@ def get_input(caller, prompt, callback, session=None, *args, **kwargs): # ------------------------------------------------------------- def _generate_goto(caller, **kwargs): - return kwargs.get("name", "text_start_node"), {"name": "replaced!"} + return kwargs.get("name", "test_dynamic_node"), {"name": "replaced!"} def test_start_node(caller): @@ -1135,7 +1148,7 @@ def test_set_node(caller): return text, options -def test_view_node(caller): +def test_view_node(caller, **kwargs): text = """ Your name is |g%s|n! @@ -1145,9 +1158,14 @@ def test_view_node(caller): -always- use numbers (1...N) to refer to listed options also if you don't see a string option key (try it!). """ % caller.key - options = {"desc": "back to main", - "goto": "test_start_node"} - return text, options + if kwargs.get("executed_from_dynamic_node", False): + # we are calling this node as a exec, skip return values + caller.msg("|gCalled from dynamic node:|n \n {}".format(text)) + return + else: + options = {"desc": "back to main", + "goto": "test_start_node"} + return text, options def test_displayinput_node(caller, raw_string): @@ -1163,20 +1181,48 @@ def test_displayinput_node(caller, raw_string): makes it hidden from view. It catches all input (except the in-menu help/quit commands) and will, in this case, bring you back to the start node. - """ % raw_string + """ % raw_string.rstrip() options = {"key": "_default", "goto": "test_start_node"} return text, options +def _test_call(caller, raw_input, **kwargs): + mode = kwargs.get("mode", "exec") + + caller.msg("\n|y'{}' |n_test_call|y function called with\n " + "caller: |n{}\n |yraw_input: \"|n{}|y\" \n kwargs: |n{}\n".format( + mode, caller, raw_input.rstrip(), kwargs)) + + if mode == "exec": + kwargs = {"random": random.random()} + caller.msg("function modify kwargs to {}".format(kwargs)) + else: + caller.msg("|ypassing function kwargs without modification.|n") + + return "test_dynamic_node", kwargs + + def test_dynamic_node(caller, **kwargs): text = """ - This is a dynamic node, whose name was - generated by the goto function. - """ - options = {} + This is a dynamic node with input: + {} + """.format(kwargs) + options = ({"desc": "pass a new random number to this node", + "goto": ("test_dynamic_node", {"random": random.random()})}, + {"desc": "execute a func with kwargs", + "exec": (_test_call, {"mode": "exec", "test_random": random.random()})}, + {"desc": "dynamic_goto", + "goto": (_test_call, {"mode": "goto", "goto_input": "test"})}, + {"desc": "exec test_view_node with kwargs", + "exec": ("test_view_node", {"executed_from_dynamic_node": True}), + "goto": "test_dynamic_node"}, + {"desc": "back to main", + "goto": "test_start_node"}) + return text, options + def test_end_node(caller): text = """ This is the end of the menu and since it has no options the menu From 931e42082cb7ea2282051838ef57e7b176842a07 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 28 Oct 2017 13:29:51 +0200 Subject: [PATCH 37/68] Make persistent evmenu's store node kwargs correctly --- evennia/utils/evmenu.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 6f31822bf3..509e2cec5f 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -270,7 +270,7 @@ class CmdEvMenuNode(Command): err = "Menu object not found as %s.ndb._menutree!" % orig_caller orig_caller.msg(err) # don't give the session as a kwarg here, direct to original raise EvMenuError(err) - # we must do this after the caller with the menui has been correctly identified since it + # we must do this after the caller with the menu has been correctly identified since it # can be either Account, Object or Session (in the latter case this info will be superfluous). caller.ndb._menutree._session = self.session # we have a menu, use it. @@ -369,9 +369,10 @@ class EvMenu(object): re-run with the same input arguments - so be careful if you are counting up some persistent counter or similar - the counter may be run twice if reload happens on the node that does that. - startnode_input (str, optional): Send an input text to `startnode` as if - a user input text from a fictional previous node. When the server reloads, - the latest visited node will be re-run using this kwarg. + startnode_input (str or (str, dict), optional): Send an input text to `startnode` as if + a user input text from a fictional previous node. If including the dict, this will + be passed as **kwargs to that node. When the server reloads, + the latest visited node will be re-run as `node(caller, raw_string, **kwargs)`. session (Session, optional): This is useful when calling EvMenu from an account in multisession mode > 2. Note that this session only really relevant for the very first display of the first node - after that, EvMenu itself @@ -428,6 +429,7 @@ class EvMenu(object): self.nodetext = None self.helptext = None self.options = None + self.node_kwargs = {} # assign kwargs as initialization vars on ourselves. if set(("_startnode", "_menutree", "_session", "_persistent", @@ -474,8 +476,13 @@ class EvMenu(object): menu_cmdset.priority = int(cmdset_priority) self.caller.cmdset.add(menu_cmdset, permanent=persistent) + startnode_kwargs = {} + if isinstance(startnode_input, (tuple, list)) and len(startnode_input) > 1: + startnode_input, startnode_kwargs = startnode_input[:2] + if not isinstance(startnode_kwargs, dict): + raise EvMenuError("startnode_input must be either a str or a tuple (str, dict).") # start the menu - self.goto(self._startnode, startnode_input) + self.goto(self._startnode, startnode_input, **startnode_kwargs) def _parse_menudata(self, menudata): """ @@ -704,7 +711,8 @@ class EvMenu(object): return if self._persistent: - self.caller.attributes.add("_menutree_saved_startnode", (nodename, raw_string)) + self.caller.attributes.add("_menutree_saved_startnode", + (nodename, (raw_string, kwargs))) # validation of the node return values helptext = "" From b6b112b70a42c467e8b6b2f2fedb820c6eb711bb Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 28 Oct 2017 14:31:09 +0200 Subject: [PATCH 38/68] Make an empty evmenu desc option just show the key --- evennia/utils/evmenu.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 509e2cec5f..d6ccd2240b 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -733,15 +733,14 @@ class EvMenu(object): for inum, dic in enumerate(options): # fix up the option dicts keys = make_iter(dic.get("key")) + desc = dic.get("desc", dic.get("text", None)) if "_default" in keys: keys = [key for key in keys if key != "_default"] - desc = dic.get("desc", dic.get("text", _ERR_NO_OPTION_DESC).strip()) goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic) self.default = (goto, goto_kwargs, execute, exec_kwargs) else: # use the key (only) if set, otherwise use the running number keys = list(make_iter(dic.get("key", str(inum + 1).strip()))) - desc = dic.get("desc", dic.get("text", _ERR_NO_OPTION_DESC).strip()) goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic) if keys: display_options.append((keys[0], desc)) @@ -887,16 +886,17 @@ class EvMenu(object): for key, desc in optionlist: if not (key or desc): continue + desc_string = ": %s" % desc if desc else "" table_width_max = max(table_width_max, max(m_len(p) for p in key.split("\n")) + - max(m_len(p) for p in desc.split("\n")) + colsep) + max(m_len(p) for p in desc_string.split("\n")) + colsep) raw_key = strip_ansi(key) if raw_key != key: # already decorations in key definition - table.append(" |lc%s|lt%s|le: %s" % (raw_key, key, desc)) + table.append(" |lc%s|lt%s|le%s" % (raw_key, key, desc_string)) else: # add a default white color to key - table.append(" |lc%s|lt|w%s|n|le: %s" % (raw_key, raw_key, desc)) + table.append(" |lc%s|lt|w%s|n|le%s" % (raw_key, raw_key, desc_string)) ncols = (_MAX_TEXT_WIDTH // table_width_max) + 1 # number of ncols @@ -1151,7 +1151,6 @@ def test_set_node(caller): """) options = {"key": ("back (default)", "_default"), - "desc": "back to main", "goto": "test_start_node"} return text, options From d05495cc52b4ae21b675c258046278756610a10d Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 28 Oct 2017 22:33:58 +0200 Subject: [PATCH 39/68] Add testing framework for EvMenu. Implements #1484 --- evennia/utils/evmenu.py | 16 ++- evennia/utils/tests/test_evmenu.py | 197 ++++++++++++++++++++++++++++- 2 files changed, 205 insertions(+), 8 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index d6ccd2240b..a595366f5c 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -429,8 +429,13 @@ class EvMenu(object): self.nodetext = None self.helptext = None self.options = None + self.nodename = None self.node_kwargs = {} + # used for testing + self.test_options = {} + self.test_nodetext = "" + # assign kwargs as initialization vars on ourselves. if set(("_startnode", "_menutree", "_session", "_persistent", "cmd_on_exit", "default", "nodetext", "helptext", @@ -605,6 +610,11 @@ class EvMenu(object): except Exception: self.caller.msg(_ERR_GENERAL.format(nodename=nodename), session=self._session) raise + + # store options to make them easier to test + self.test_options = options + self.test_nodetext = nodetext + return nodetext, options def run_exec(self, nodename, raw_string, **kwargs): @@ -750,6 +760,8 @@ class EvMenu(object): (goto, goto_kwargs, execute, exec_kwargs) self.nodetext = self._format_node(nodetext, display_options) + self.node_kwargs = kwargs + self.nodename = nodename # handle the helptext if helptext: @@ -815,7 +827,7 @@ class EvMenu(object): should also report errors directly to the user. """ - cmd = raw_string.strip().lower() + cmd = strip_ansi(raw_string.strip().lower()) if cmd in self.options: # this will take precedence over the default commands @@ -1124,7 +1136,7 @@ def test_start_node(caller): def test_look_node(caller): - text = "" + text = "This is a custom look location!" options = {"key": ("|yL|nook", "l"), "desc": "Go back to the previous menu.", "goto": "test_start_node"} diff --git a/evennia/utils/tests/test_evmenu.py b/evennia/utils/tests/test_evmenu.py index 4436fdccd6..a3517c35ac 100644 --- a/evennia/utils/tests/test_evmenu.py +++ b/evennia/utils/tests/test_evmenu.py @@ -5,20 +5,205 @@ TODO: This need expansion. """ +import copy from django.test import TestCase from evennia.utils import evmenu -from mock import Mock +from evennia.utils import ansi +from mock import MagicMock class TestEvMenu(TestCase): "Run the EvMenu testing." + menutree = {} # can also be the path to the menu tree + startnode = "start" + cmdset_mergetype = "Replace" + cmdset_priority = 1 + auto_quit = True + auto_look = True + auto_help = True + cmd_on_exit = "look" + persistent = False + startnode_input = "" + kwargs = {} + + # this is compared against the full tree structure generated + expected_tree = [] + # this allows for verifying that a given node returns a given text. The + # text is compared with .startswith, so the entire text need not be matched. + expected_node_texts = {} + # just check the number of options from each node + expected_node_options_count = {} + # check the actual options + expected_node_options = {} + + # set this to print the traversal as it happens (debugging) + debug_output = False + + def _debug_output(self, indent, msg): + if self.debug_output: + print(" " * indent + msg) + + def _test_menutree(self, menu): + """ + This is a automatic tester of the menu tree by recursively progressing through the + structure. + """ + + def _depth_first(menu, tree, visited, indent): + + # we are in a given node here + nodename = menu.nodename + options = menu.test_options + if isinstance(options, dict): + options = (options, ) + + # run validation tests for this node + compare_text = self.expected_node_texts.get(nodename, None) + if compare_text is not None: + compare_text = ansi.strip_ansi(compare_text.strip()) + node_text = menu.test_nodetext + self.assertIsNotNone( + bool(node_text), + "node: {}: node-text is None, which was not expected.".format(nodename)) + node_text = ansi.strip_ansi(node_text.strip()) + self.assertTrue( + node_text.startswith(compare_text), + "\nnode \"{}\':\nOutput:\n{}\n\nExpected (startswith):\n{}".format( + nodename, node_text, compare_text)) + compare_options_count = self.expected_node_options_count.get(nodename, None) + if compare_options_count is not None: + self.assertEqual( + len(options), compare_options_count, + "Not the right number of options returned from node {}.".format(nodename)) + compare_options = self.expected_node_options.get(nodename, None) + if compare_options: + self.assertEqual( + options, compare_options, + "Options returned from node {} does not match.".format(nodename)) + + self._debug_output(indent, "*{}".format(nodename)) + subtree = [] + + if not options: + # an end node + if nodename not in visited: + visited.append(nodename) + subtree = nodename + else: + for inum, optdict in enumerate(options): + + key, desc, execute, goto = optdict.get("key", ""), optdict.get("desc", None),\ + optdict.get("exec", None), optdict.get("goto", None) + + # prepare the key to pass to the menu + if isinstance(key, (tuple, list)) and len(key) > 1: + key = key[0] + if key == "_default": + key = "test raw input" + if not key: + key = str(inum + 1) + + backup_menu = copy.copy(menu) + + # step the menu + menu.parse_input(key) + + # from here on we are likely in a different node + nodename = menu.nodename + + if menu.close_menu.called: + # this was an end node + self._debug_output(indent, " .. menu exited! Back to previous node.") + menu = backup_menu + menu.close_menu = MagicMock() + visited.append(nodename) + subtree.append(nodename) + elif nodename not in visited: + visited.append(nodename) + subtree.append(nodename) + _depth_first(menu, subtree, visited, indent + 2) + #self._debug_output(indent, " -> arrived at {}".format(nodename)) + else: + subtree.append(nodename) + #self._debug_output( indent, " -> arrived at {} (circular call)".format(nodename)) + self._debug_output(indent, "-- {} ({}) -> {}".format(key, desc, goto)) + + if subtree: + tree.append(subtree) + + # the start node has already fired at this point + visited_nodes = [menu.nodename] + traversal_tree = [menu.nodename] + _depth_first(menu, traversal_tree, visited_nodes, 1) + + self.assertGreaterEqual(len(menu._menutree), len(visited_nodes)) + self.assertEqual(traversal_tree, self.expected_tree) def setUp(self): - self.caller = Mock() - self.caller.msg = Mock() - self.menu = evmenu.EvMenu(self.caller, "evennia.utils.evmenu", startnode="test_start_node", - persistent=True, cmdset_mergetype="Replace", testval="val", - testval2="val2") + self.menu = None + if self.menutree: + self.caller = MagicMock() + self.caller.key = "Test" + self.caller2 = MagicMock() + self.caller2.key = "Test" + self.caller.msg = MagicMock() + self.caller2.msg = MagicMock() + self.session = MagicMock() + self.session2 = MagicMock() + self.menu = evmenu.EvMenu(self.caller, self.menutree, startnode=self.startnode, + cmdset_mergetype=self.cmdset_mergetype, + cmdset_priority=self.cmdset_priority, + auto_quit=self.auto_quit, auto_look=self.auto_look, + auto_help=self.auto_help, + cmd_on_exit=self.cmd_on_exit, persistent=False, + startnode_input=self.startnode_input, session=self.session, + **self.kwargs) + # persistent version + self.pmenu = evmenu.EvMenu(self.caller2, self.menutree, startnode=self.startnode, + cmdset_mergetype=self.cmdset_mergetype, + cmdset_priority=self.cmdset_priority, + auto_quit=self.auto_quit, auto_look=self.auto_look, + auto_help=self.auto_help, + cmd_on_exit=self.cmd_on_exit, persistent=True, + startnode_input=self.startnode_input, session=self.session2, + **self.kwargs) + + self.menu.close_menu = MagicMock() + self.pmenu.close_menu = MagicMock() + + def test_menu_structure(self): + if self.menu: + self._test_menutree(self.menu) + self._test_menutree(self.pmenu) + + +class TestEvMenuExample(TestEvMenu): + + menutree = "evennia.utils.evmenu" + startnode = "test_start_node" + kwargs = {"testval": "val", "testval2": "val2"} + debug_output = False + + expected_node_texts = { + "test_view_node": "Your name is"} + + expected_tree = \ + ['test_start_node', + ['test_set_node', + ['test_start_node'], + 'test_look_node', + ['test_start_node'], + 'test_view_node', + ['test_start_node'], + 'test_dynamic_node', + ['test_dynamic_node', + 'test_dynamic_node', + 'test_dynamic_node', + 'test_dynamic_node', + 'test_start_node'], + 'test_end_node', + 'test_displayinput_node', + ['test_start_node']]] def test_kwargsave(self): self.assertTrue(hasattr(self.menu, "testval")) From 65664bf523996e7c7b07d67c9c5d72d45973a3d8 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 28 Oct 2017 22:48:06 +0200 Subject: [PATCH 40/68] Add documentation to EvMenu test class --- evennia/utils/tests/test_evmenu.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/evennia/utils/tests/test_evmenu.py b/evennia/utils/tests/test_evmenu.py index a3517c35ac..7f01db63f6 100644 --- a/evennia/utils/tests/test_evmenu.py +++ b/evennia/utils/tests/test_evmenu.py @@ -1,7 +1,17 @@ """ Unit tests for the EvMenu system -TODO: This need expansion. +This sets up a testing parent for testing EvMenu trees. It is configured by subclassing the +`TestEvMenu` class from this module and setting the class variables to point to the menu that should +be tested and how it should be called. + +Without adding any further test methods, the tester will process all nodes of the menu, width first, +by stepping through all options for every node. It will check to make sure all are visited. It will +create a hierarchical list of node names that describes the tree structure. Easiest way to use this +is to run the test once to see how the structure looks. + +The system also allows for testing the returns of each node as part of the parsing. To help debug +the menu, turn on `debug_output`, which will print the traversal process in detail. """ @@ -26,6 +36,11 @@ class TestEvMenu(TestCase): startnode_input = "" kwargs = {} + # if all nodes must be visited for the test to pass. This is not on + # by default since there may be exec-nodes that are made to not be + # visited. + expect_all_nodes = False + # this is compared against the full tree structure generated expected_tree = [] # this allows for verifying that a given node returns a given text. The @@ -136,7 +151,8 @@ class TestEvMenu(TestCase): traversal_tree = [menu.nodename] _depth_first(menu, traversal_tree, visited_nodes, 1) - self.assertGreaterEqual(len(menu._menutree), len(visited_nodes)) + if self.expect_all_nodes: + self.assertGreaterEqual(len(menu._menutree), len(visited_nodes)) self.assertEqual(traversal_tree, self.expected_tree) def setUp(self): From a5a8d9dd5735e4beca76714c8e93cf4fcfbe6f0c Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 28 Oct 2017 22:51:47 +0200 Subject: [PATCH 41/68] Some doc updates --- evennia/utils/tests/test_evmenu.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/evennia/utils/tests/test_evmenu.py b/evennia/utils/tests/test_evmenu.py index 7f01db63f6..04310c90ed 100644 --- a/evennia/utils/tests/test_evmenu.py +++ b/evennia/utils/tests/test_evmenu.py @@ -5,13 +5,15 @@ This sets up a testing parent for testing EvMenu trees. It is configured by subc `TestEvMenu` class from this module and setting the class variables to point to the menu that should be tested and how it should be called. -Without adding any further test methods, the tester will process all nodes of the menu, width first, -by stepping through all options for every node. It will check to make sure all are visited. It will -create a hierarchical list of node names that describes the tree structure. Easiest way to use this -is to run the test once to see how the structure looks. +Without adding any further test methods, the tester will process all nodes of the menu, depth first, +by stepping through all options for every node. Optionally, it can check that all nodes are visited. +It will create a hierarchical list of node names that describes the tree structure. This can then be +compared against a template to make sure the menu structure is sound. Easiest way to use this is to +run the test once to see how the structure looks. -The system also allows for testing the returns of each node as part of the parsing. To help debug -the menu, turn on `debug_output`, which will print the traversal process in detail. +The system also allows for testing the returns of each node as part of the parsing. + +To help debug the menu, turn on `debug_output`, which will print the traversal process in detail. """ From 9bc3fcf4860a3a1477940ce345e02c6b870758ff Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 29 Oct 2017 21:39:11 -0700 Subject: [PATCH 42/68] Unit tests for tb_range added --- evennia/contrib/tests.py | 110 ++++++++++++++++++++++++- evennia/contrib/turnbattle/tb_range.py | 6 +- 2 files changed, 111 insertions(+), 5 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 1fdd7dde75..4b2c1a696f 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -907,7 +907,7 @@ class TestTutorialWorldRooms(CommandTest): # test turnbattle -from evennia.contrib.turnbattle import tb_basic, tb_equip +from evennia.contrib.turnbattle import tb_basic, tb_equip, tb_range from evennia.objects.objects import DefaultRoom @@ -939,11 +939,25 @@ class TestTurnBattleCmd(CommandTest): self.call(tb_equip.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") self.call(tb_equip.CmdRest(), "", "Char rests to recover HP.") + # Test range commands + def test_turnbattlerangecmd(self): + # Start with range module specific commands. + self.call(tb_range.CmdShoot(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_range.CmdApproach(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_range.CmdWithdraw(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_range.CmdStatus(), "", "HP Remaining: 100 / 100") + # Also test the commands that are the same in the basic module + self.call(tb_range.CmdFight(), "", "There's nobody here to fight!") + self.call(tb_range.CmdAttack(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_range.CmdPass(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_range.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") + self.call(tb_range.CmdRest(), "", "Char rests to recover HP.") + class TestTurnBattleFunc(EvenniaTest): # Test combat functions - def test_turnbattlefunc(self): + def test_tbbasicfunc(self): attacker = create_object(tb_basic.TBBasicCharacter, key="Attacker") defender = create_object(tb_basic.TBBasicCharacter, key="Defender") testroom = create_object(DefaultRoom, key="Test Room") @@ -1020,7 +1034,7 @@ class TestTurnBattleFunc(EvenniaTest): turnhandler.stop() # Test the combat functions in tb_equip too. They work mostly the same. - def test_turnbattlefunc(self): + def test_tbequipfunc(self): attacker = create_object(tb_equip.TBEquipCharacter, key="Attacker") defender = create_object(tb_equip.TBEquipCharacter, key="Defender") testroom = create_object(DefaultRoom, key="Test Room") @@ -1095,6 +1109,96 @@ class TestTurnBattleFunc(EvenniaTest): self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender]) # Remove the script at the end turnhandler.stop() + + # Test combat functions in tb_range too. + def test_tbrangefunc(self): + testroom = create_object(DefaultRoom, key="Test Room") + attacker = create_object(tb_range.TBRangeCharacter, key="Attacker", location=testroom) + defender = create_object(tb_range.TBRangeCharacter, key="Defender", location=testroom) + # Initiative roll + initiative = tb_range.roll_init(attacker) + self.assertTrue(initiative >= 0 and initiative <= 1000) + # Attack roll + attack_roll = tb_range.get_attack(attacker, defender, "test") + self.assertTrue(attack_roll >= 0 and attack_roll <= 100) + # Defense roll + defense_roll = tb_range.get_defense(attacker, defender, "test") + self.assertTrue(defense_roll == 50) + # Damage roll + damage_roll = tb_range.get_damage(attacker, defender) + self.assertTrue(damage_roll >= 15 and damage_roll <= 25) + # Apply damage + defender.db.hp = 10 + tb_range.apply_damage(defender, 3) + self.assertTrue(defender.db.hp == 7) + # Resolve attack + defender.db.hp = 40 + tb_range.resolve_attack(attacker, defender, "test", attack_value=20, defense_value=10) + self.assertTrue(defender.db.hp < 40) + # Combat cleanup + attacker.db.Combat_attribute = True + tb_range.combat_cleanup(attacker) + self.assertFalse(attacker.db.combat_attribute) + # Is in combat + self.assertFalse(tb_range.is_in_combat(attacker)) + # Set up turn handler script for further tests + attacker.location.scripts.add(tb_range.TBRangeTurnHandler) + turnhandler = attacker.db.combat_TurnHandler + self.assertTrue(attacker.db.combat_TurnHandler) + # Force turn order + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + # Test is turn + self.assertTrue(tb_range.is_turn(attacker)) + # Spend actions + attacker.db.Combat_ActionsLeft = 1 + tb_range.spend_action(attacker, 1, action_name="Test") + self.assertTrue(attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(attacker.db.Combat_LastAction == "Test") + # Initialize for combat + attacker.db.Combat_ActionsLeft = 983 + turnhandler.initialize_for_combat(attacker) + self.assertTrue(attacker.db.Combat_ActionsLeft == 0) + self.assertTrue(attacker.db.Combat_LastAction == "null") + # Set up ranges again, since initialize_for_combat clears them + attacker.db.combat_range = {} + attacker.db.combat_range[attacker] = 0 + attacker.db.combat_range[defender] = 1 + defender.db.combat_range = {} + defender.db.combat_range[defender] = 0 + defender.db.combat_range[attacker] = 1 + # Start turn + defender.db.Combat_ActionsLeft = 0 + turnhandler.start_turn(defender) + self.assertTrue(defender.db.Combat_ActionsLeft == 2) + # Next turn + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + turnhandler.next_turn() + self.assertTrue(turnhandler.db.turn == 1) + # Turn end check + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + attacker.db.Combat_ActionsLeft = 0 + turnhandler.turn_end_check(attacker) + self.assertTrue(turnhandler.db.turn == 1) + # Join fight + joiner = create_object(tb_range.TBRangeCharacter, key="Joiner", location=testroom) + turnhandler.db.fighters = [attacker, defender] + turnhandler.db.turn = 0 + turnhandler.join_fight(joiner) + self.assertTrue(turnhandler.db.turn == 1) + self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender]) + # Now, test for approach/withdraw functions + self.assertTrue(tb_range.get_range(attacker, defender) == 1) + # Approach + tb_range.approach(attacker, defender) + self.assertTrue(tb_range.get_range(attacker, defender) == 0) + # Withdraw + tb_range.withdraw(attacker, defender) + self.assertTrue(tb_range.get_range(attacker, defender) == 1) + # Remove the script at the end + turnhandler.stop() # Test of the unixcommand module diff --git a/evennia/contrib/turnbattle/tb_range.py b/evennia/contrib/turnbattle/tb_range.py index 0eb617c48b..5596573a2d 100644 --- a/evennia/contrib/turnbattle/tb_range.py +++ b/evennia/contrib/turnbattle/tb_range.py @@ -474,7 +474,10 @@ def combat_status_message(fighter): distances to other fighters and objects. Called at turn start and by the 'status' command. """ - + if not fighter.db.max_hp: + fighter.db.hp = 100 + fighter.db.max_hp = 100 + status_msg = ("HP Remaining: %i / %i" % (fighter.db.hp, fighter.db.max_hp)) if not is_in_combat(fighter): @@ -1326,7 +1329,6 @@ class CmdStatus(Command): def func(self): "This performs the actual command." - combat_status_message(self.caller) From df9072253f9a99986689e397b66dcee90c0b2b6e Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 29 Oct 2017 23:00:22 -0700 Subject: [PATCH 43/68] Set turn handler's intervals higher during tests This was an attempt to try to fix some strange 'unhandled error in deffered' results while unit testing the contrib folder. It didn't work, but it's probably good to do anyway. --- evennia/contrib/tests.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 4b2c1a696f..79a61ce65d 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -993,6 +993,8 @@ class TestTurnBattleFunc(EvenniaTest): attacker.location.scripts.add(tb_basic.TBBasicTurnHandler) turnhandler = attacker.db.combat_TurnHandler self.assertTrue(attacker.db.combat_TurnHandler) + # Set the turn handler's interval very high to keep it from repeating during tests. + turnhandler.interval = 10000 # Force turn order turnhandler.db.fighters = [attacker, defender] turnhandler.db.turn = 0 @@ -1070,6 +1072,8 @@ class TestTurnBattleFunc(EvenniaTest): attacker.location.scripts.add(tb_equip.TBEquipTurnHandler) turnhandler = attacker.db.combat_TurnHandler self.assertTrue(attacker.db.combat_TurnHandler) + # Set the turn handler's interval very high to keep it from repeating during tests. + turnhandler.interval = 10000 # Force turn order turnhandler.db.fighters = [attacker, defender] turnhandler.db.turn = 0 @@ -1145,6 +1149,8 @@ class TestTurnBattleFunc(EvenniaTest): attacker.location.scripts.add(tb_range.TBRangeTurnHandler) turnhandler = attacker.db.combat_TurnHandler self.assertTrue(attacker.db.combat_TurnHandler) + # Set the turn handler's interval very high to keep it from repeating during tests. + turnhandler.interval = 10000 # Force turn order turnhandler.db.fighters = [attacker, defender] turnhandler.db.turn = 0 From c6f422d44b1bfe81e0cb7c7cdb1eafb9737d4c8a Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 30 Oct 2017 05:03:26 -0700 Subject: [PATCH 44/68] Updated username in contrib readme --- evennia/contrib/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/README.md b/evennia/contrib/README.md index ed3c048c32..e37b1ee294 100644 --- a/evennia/contrib/README.md +++ b/evennia/contrib/README.md @@ -19,7 +19,7 @@ things you want from here into your game folder and change them there. for any game. Allows safe trading of any godds (including coin) * CharGen (Griatch 2011) - A simple Character creator for OOC mode. Meant as a starting point for a more fleshed-out system. -* Clothing (BattleJenkins 2017) - A layered clothing system with +* Clothing (FlutterSprite 2017) - A layered clothing system with slots for different types of garments auto-showing in description. * Color-markups (Griatch, 2017) - Alternative in-game color markups. * Custom gametime (Griatch, vlgeoff 2017) - Implements Evennia's @@ -50,7 +50,7 @@ things you want from here into your game folder and change them there. time to pass depending on if you are walking/running etc. * Talking NPC (Griatch 2011) - A talking NPC object that offers a menu-driven conversation tree. -* Turnbattle (BattleJenkins 2017) - A turn-based combat engine meant +* Turnbattle (FlutterSprite 2017) - A turn-based combat engine meant as a start to build from. Has attack/disengage and turn timeouts. * Wilderness (titeuf87 2017) - Make infinitely large wilderness areas with dynamically created locations. From 89773e58608660f49b56d0c0a73169fdc52f76c6 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 30 Oct 2017 05:11:53 -0700 Subject: [PATCH 45/68] Moved 'turnbattle' to packages section in readme --- evennia/contrib/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/README.md b/evennia/contrib/README.md index e37b1ee294..63abb2f713 100644 --- a/evennia/contrib/README.md +++ b/evennia/contrib/README.md @@ -50,8 +50,6 @@ things you want from here into your game folder and change them there. time to pass depending on if you are walking/running etc. * Talking NPC (Griatch 2011) - A talking NPC object that offers a menu-driven conversation tree. -* Turnbattle (FlutterSprite 2017) - A turn-based combat engine meant - as a start to build from. Has attack/disengage and turn timeouts. * Wilderness (titeuf87 2017) - Make infinitely large wilderness areas with dynamically created locations. * UnixCommand (Vincent Le Geoff 2017) - Add commands with UNIX-style syntax. @@ -62,6 +60,9 @@ things you want from here into your game folder and change them there. to the Evennia game index (games.evennia.com) * In-game Python (Vincent Le Goff 2017) - Allow trusted builders to script objects and events using Python from in-game. +* Turnbattle (FlutterSprite 2017) - A turn-based combat engine meant + as a start to build from. Has attack/disengage and turn timeouts, + and includes optional expansions for equipment and combat movement. * Tutorial examples (Griatch 2011, 2015) - A folder of basic example objects, commands and scripts. * Tutorial world (Griatch 2011, 2015) - A folder containing the From d1aa757ba9532ee73eef51d136848d5a6c7fa225 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 30 Oct 2017 13:23:06 -0700 Subject: [PATCH 46/68] Clarify the nature of the different modules --- evennia/contrib/turnbattle/README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/evennia/contrib/turnbattle/README.md b/evennia/contrib/turnbattle/README.md index af04f80060..729c42a099 100644 --- a/evennia/contrib/turnbattle/README.md +++ b/evennia/contrib/turnbattle/README.md @@ -26,9 +26,17 @@ implemented and customized: tracks the distance between different characters and objects in combat, as well as differentiates between melee and ranged attacks. - + This system is meant as a basic framework to start from, and is modeled after the combat systems of popular tabletop role playing games rather than the real-time battle systems that many MMOs and some MUDs use. As such, it may be better suited to role-playing or more story-oriented games, or games meant to closely emulate the experience of playing a tabletop RPG. + +Each of these modules contains the full functionality of the battle system +with different customizations added in - the instructions to install each +one is contained in the module itself. It's recommended that you install +and test tb_basic first, so you can better understand how the other +modules expand on it and get a better idea of how you can customize the +system to your liking and integrate the subsystems presented here into +your own combat system. From 8b95b4718ae236111fb1c33295b6a38cdb9b20d2 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 30 Oct 2017 15:01:51 -0700 Subject: [PATCH 47/68] Add simple menu tree selection contrib This contrib module allows developers to generate an EvMenu instance with options sourced from a multi-line string, which supports categories, back and forth menu navigation, option descriptions, and passing selections to custom callbacks. This allows for easier dynamic menus and much faster deployment of simple menu trees which does not require the manual definition of menu nodes and option dictionary-lists. --- evennia/contrib/tree_select.py | 522 +++++++++++++++++++++++++++++++++ 1 file changed, 522 insertions(+) create mode 100644 evennia/contrib/tree_select.py diff --git a/evennia/contrib/tree_select.py b/evennia/contrib/tree_select.py new file mode 100644 index 0000000000..56305340b3 --- /dev/null +++ b/evennia/contrib/tree_select.py @@ -0,0 +1,522 @@ +""" +Easy menu selection tree + +Contrib - Tim Ashley Jenkins 2017 + +This module allows you to create and initialize an entire branching EvMenu +instance with nothing but a multi-line string passed to one function. + +EvMenu is incredibly powerful and flexible, but using it for simple menus +can often be fairly cumbersome - a simple menu that can branch into five +categories would require six nodes, each with options represented as a list +of dictionaries. + +This module provides a function, init_tree_selection, which acts as a frontend +for EvMenu, dynamically sourcing the options from a multi-line string you provide. +For example, if you define a string as such: + + TEST_MENU = '''Foo + Bar + Baz + Qux''' + +And then use TEST_MENU as the 'treestr' source when you call init_tree_selection +on a player: + + init_tree_selection(TEST_MENU, caller, callback) + +The player will be presented with an EvMenu, like so: + + ___________________________ + + Make your selection: + ___________________________ + + Foo + Bar + Baz + Qux + +Making a selection will pass the selection's key to the specified callback as a +string along with the caller, as well as the index of the selection (the line number +on the source string) along with the source string for the tree itself. + +In addition to specifying selections on the menu, you can also specify categories. +Categories are indicated by putting options below it preceded with a '-' character. +If a selection is a category, then choosing it will bring up a new menu node, prompting +the player to select between those options, or to go back to the previous menu. In +addition, categories are marked by default with a '[+]' at the end of their key. Both +this marker and the option to go back can be disabled. + +Categories can be nested in other categories as well - just go another '-' deeper. You +can do this as many times as you like. There's no hard limit to the number of +categories you can go down. + +For example, let's add some more options to our menu, turning 'Foo' into a category. + + TEST_MENU = '''Foo + Bar + -You've got to know + --When to hold em + --When to fold em + --When to walk away + Baz + Qux''' + +Now when we call the menu, we can see that 'Foo' has become a category instead of a +selectable option. + + _______________________________ + + Make your selection: + _______________________________ + + Foo + Bar [+] + Baz + Qux + +Note the [+] next to 'Bar'. If we select 'Bar', it'll show us the option listed under it. + + ________________________________________________________________ + + Bar + ________________________________________________________________ + + You've got to know [+] + << Go Back: Return to the previous menu. + +Just the one option, which is a category itself, and the option to go back, which will +take us back to the previous menu. Let's select 'You've got to know'. + + ________________________________________________________________ + + You've got to know + ________________________________________________________________ + + When to hold em + When to fold em + When to walk away + << Go Back: Return to the previous menu. + +Now we see the three options listed under it, too. We can select one of them or use 'Go +Back' to return to the 'Bar' menu we were just at before. It's very simple to make a +branching tree of selections! + +One last thing - you can set the descriptions for the various options simply by adding a +':' character followed by the description to the option's line. For example, let's add a +description to 'Baz' in our menu: + + TEST_MENU = '''Foo + Bar + -You've got to know + --When to hold em + --When to fold em + --When to walk away + Baz: Look at this one: the best option. + Qux''' + +Now we see that the Baz option has a description attached that's separate from its key: + + _______________________________________________________________ + + Make your selection: + _______________________________________________________________ + + Foo + Bar [+] + Baz: Look at this one: the best option. + Qux + +And that's all there is to it! For simple branching-tree selections, using this system is +much easier than manually creating EvMenu nodes. It also makes generating menus with dynamic +options much easier - since the source of the menu tree is just a string, you could easily +generate that string procedurally before passing it to the init_tree_selection function. +For example, if a player casts a spell or does an attack without specifying a target, instead +of giving them an error, you could present them with a list of valid targets to select by +generating a multi-line string of targets and passing it to init_tree_selection, with the +callable performing the maneuver once a selection is made. + +This selection system only works for simple branching trees - doing anything really complicated +like jumping between categories or prompting for arbitrary input would still require a full +EvMenu implementation. For simple selections, however, I'm sure you will find using this function +to be much easier! + +Included in this module is a sample menu and function which will let a player change the color +of their name - feel free to mess with it to get a feel for how this system works by importing +this module in your game's default_cmdsets.py module and adding CmdNameColor to your default +character's command set. +""" + +from evennia.utils import evmenu +from evennia import Command + +def init_tree_selection(treestr, caller, callback, + index=None, mark_category=True, go_back=True, + cmd_on_exit="look", + start_text="Make your selection:"): + """ + Prompts a player to select an option from a menu tree given as a multi-line string. + + Args: + treestr (str): Multi-lne string representing menu options + caller (obj): Player to initialize the menu for + callback (callable): Function to run when a selection is made. Must take 4 args: + treestr (str): Menu tree string given above + caller (obj): Caller given above + index (int): Index of final selection + selection (str): Key of final selection + + Options: + index (int or None): Index to start the menu at, or None for top level + mark_category (bool): If True, marks categories with a [+] symbol in the menu + go_back (bool): If True, present an option to go back to previous categories + start_text (str): Text to display at the top level of the menu + cmd_on_exit(str): Command to enter when the menu exits - 'look' by default + + + Notes: + This function will initialize an instance of EvMenu with options generated + dynamically from the source string, and passes the menu user's selection to + a function of your choosing. The EvMenu is made of a single, repeating node, + which will call itself over and over at different levels of the menu tree as + categories are selected. + + Once a non-category selection is made, the user's selection will be passed to + the given callable, both as a string and as an index number. The index is given + to ensure every selection has a unique identifier, so that selections with the + same key in different categories can be distinguished between. + + The menus called by this function are not persistent and cannot perform + complicated tasks like prompt for arbitrary input or jump multiple category + levels at once - you'll have to use EvMenu itself if you want to take full + advantage of its features. + """ + + # Pass kwargs to store data needed in the menu + kwargs = { + "index":index, + "mark_category":mark_category, + "go_back":go_back, + "treestr":treestr, + "callback":callback, + "start_text":start_text + } + + # Initialize menu of selections + evmenu.EvMenu(caller, "evennia.contrib.tree_select", startnode="menunode_treeselect", + startnode_input=None, cmd_on_exit=cmd_on_exit, **kwargs) + +def dashcount(entry): + """ + Counts the number of dashes at the beginning of a string. This + is needed to determine the depth of options in categories. + + Args: + entry (str): String to count the dashes at the start of + + Returns: + dashes (int): Number of dashes at the start + """ + dashes = 0 + for char in entry: + if char == "-": + dashes += 1 + else: + return dashes + return dashes + +def is_category(treestr, index): + """ + Determines whether an option in a tree string is a category by + whether or not there are additional options below it. + + Args: + treestr (str): Multi-line string representing menu options + index (int): Which line of the string to test + + Returns: + is_category (bool): Whether the option is a category + """ + opt_list = treestr.split('\n') + # Not a category if it's the last one in the list + if index == len(opt_list) - 1: + return False + # Not a category if next option is not one level deeper + return not bool(dashcount(opt_list[index+1]) != dashcount(opt_list[index]) + 1) + +def parse_opts(treestr, category_index=None): + """ + Parses a tree string and given index into a list of options. If + category_index is none, returns all the options at the top level of + the menu. If category_index corresponds to a category, returns a list + of options under that category. If category_index corresponds to + an option that is not a category, it's a selection and returns True. + + Args: + treestr (str): Multi-line string representing menu options + category_index (int): Index of category or None for top level + + Returns: + kept_opts (list or True): Either a list of options in the selected + category or True if a selection was made + """ + dash_depth = 0 + opt_list = treestr.split('\n') + kept_opts = [] + + # If a category index is given + if category_index != None: + # If given index is not a category, it's a selection - return True. + if not is_category(treestr, category_index): + return True + # Otherwise, change the dash depth to match the new category. + dash_depth = dashcount(opt_list[category_index]) + 1 + # Delete everything before the category index + opt_list = opt_list [category_index+1:] + + # Keep every option (referenced by index) at the appropriate depth + cur_index = 0 + for option in opt_list: + if dashcount(option) == dash_depth: + if category_index == None: + kept_opts.append((cur_index, option[dash_depth:])) + else: + kept_opts.append((cur_index + category_index + 1, option[dash_depth:])) + # Exits the loop if leaving a category + if dashcount(option) < dash_depth: + return kept_opts + cur_index += 1 + return kept_opts + +def index_to_selection(treestr, index, desc=False): + """ + Given a menu tree string and an index, returns the corresponding selection's + name as a string. If 'desc' is set to True, will return the selection's + description as a string instead. + + Args: + treestr (str): Multi-line string representing menu options + index (int): Index to convert to selection key or description + + Options: + desc (bool): If true, returns description instead of key + + Returns: + selection (str): Selection key or description if 'desc' is set + """ + opt_list = treestr.split('\n') + # Fetch the given line + selection = opt_list[index] + # Strip out the dashes at the start + selection = selection[dashcount(selection):] + # Separate out description, if any + if ":" in selection: + # Split string into key and description + selection = selection.split(':', 1) + selection[1] = selection[1].strip(" ") + else: + # If no description given, set description to None + selection = [selection, None] + if not desc: + return selection[0] + else: + return selection[1] + +def go_up_one_category(treestr, index): + """ + Given a menu tree string and an index, returns the category that the given option + belongs to. Used for the 'go back' option. + + Args: + treestr (str): Multi-line string representing menu options + index (int): Index to determine the parent category of + + Returns: + parent_category (int): Index of parent category + """ + opt_list = treestr.split('\n') + # Get the number of dashes deep the given index is + dash_level = dashcount(opt_list[index]) + # Delete everything after the current index + opt_list = opt_list[:index+1] + + + # If there's no dash, return 'None' to return to base menu + if dash_level == 0: + return None + current_index = index + # Go up through each option until we find one that's one category above + for selection in reversed(opt_list): + if dashcount(selection) == dash_level - 1: + return current_index + current_index -= 1 + +def optlist_to_menuoptions(treestr, optlist, index, mark_category, go_back): + """ + Takes a list of options processed by parse_opts and turns it into + a list/dictionary of menu options for use in menunode_treeselect. + + Args: + treestr (str): Multi-line string representing menu options + optlist (list): List of options to convert to EvMenu's option format + index (int): Index of current category + mark_category (bool): Whether or not to mark categories with [+] + go_back (bool): Whether or not to add an option to go back in the menu + + Returns: + menuoptions (list of dicts): List of menu options formatted for use + in EvMenu, each passing a different "newindex" kwarg that changes + the menu level or makes a selection + """ + + menuoptions = [] + cur_index = 0 + for option in optlist: + index_to_add = optlist[cur_index][0] + menuitem = {} + keystr = index_to_selection(treestr, index_to_add) + if mark_category and is_category(treestr, index_to_add): + # Add the [+] to the key if marking categories, and the key by itself as an alias + menuitem["key"] = [keystr + " [+]", keystr] + else: + menuitem["key"] = keystr + # Get the option's description + desc = index_to_selection(treestr, index_to_add, desc=True) + if desc: + menuitem["desc"] = desc + # Passing 'newindex' as a kwarg to the node is how we move through the menu! + menuitem["goto"] = ["menunode_treeselect", {"newindex":index_to_add}] + menuoptions.append(menuitem) + cur_index += 1 + # Add option to go back, if needed + if index != None and go_back == True: + gobackitem = {"key":["<< Go Back", "go back", "back"], + "desc":"Return to the previous menu.", + "goto":["menunode_treeselect", {"newindex":go_up_one_category(treestr, index)}]} + menuoptions.append(gobackitem) + return menuoptions + +def menunode_treeselect(caller, raw_string, **kwargs): + """ + This is the repeating menu node that handles the tree selection. + """ + + # If 'newindex' is in the kwargs, change the stored index. + if "newindex" in kwargs: + caller.ndb._menutree.index = kwargs["newindex"] + + # Retrieve menu info + index = caller.ndb._menutree.index + mark_category = caller.ndb._menutree.mark_category + go_back = caller.ndb._menutree.go_back + treestr = caller.ndb._menutree.treestr + callback = caller.ndb._menutree.callback + start_text = caller.ndb._menutree.start_text + + # List of options if index is 'None' or category, or 'True' if a selection + optlist = parse_opts(treestr, category_index=index) + + # If given index returns optlist as 'True', it's a selection. Pass to callback and end the menu. + if optlist == True: + selection = index_to_selection(treestr, index) + callback(caller, treestr, index, selection) + + # Returning None, None ends the menu. + return None, None + + # Otherwise, convert optlist to a list of menu options. + else: + options = optlist_to_menuoptions(treestr, optlist, index, mark_category, go_back) + if index == None: + # Use start_text for the menu text on the top level + text = start_text + else: + # Use the category name and description (if any) as the menu text + if index_to_selection(treestr, index, desc=True) != None: + text = "|w" + index_to_selection(treestr, index) + "|n: " + index_to_selection(treestr, index, desc=True) + else: + text = "|w" + index_to_selection(treestr, index) + "|n" + return text, options + +# The rest of this module is for the example menu and command! It'll change the color of your name. + +""" +Here's an example string that you can initialize a menu from. Note the dashes at +the beginning of each line - that's how menu option depth and hierarchy is determined. +""" + +NAMECOLOR_MENU = """Set name color: Choose a color for your name! +-Red shades: Various shades of |511red|n +--Red: |511Set your name to Red|n +--Pink: |533Set your name to Pink|n +--Maroon: |301Set your name to Maroon|n +-Orange shades: Various shades of |531orange|n +--Orange: |531Set your name to Orange|n +--Brown: |321Set your name to Brown|n +--Sienna: |420Set your name to Sienna|n +-Yellow shades: Various shades of |551yellow|n +--Yellow: |551Set your name to Yellow|n +--Gold: |540Set your name to Gold|n +--Dandelion: |553Set your name to Dandelion|n +-Green shades: Various shades of |141green|n +--Green: |141Set your name to Green|n +--Lime: |350Set your name to Lime|n +--Forest: |032Set your name to Forest|n +-Blue shades: Various shades of |115blue|n +--Blue: |115Set your name to Blue|n +--Cyan: |155Set your name to Cyan|n +--Navy: |113Set your name to Navy|n +-Purple shades: Various shades of |415purple|n +--Purple: |415Set your name to Purple|n +--Lavender: |535Set your name to Lavender|n +--Fuchsia: |503Set your name to Fuchsia|n +Remove name color: Remove your name color, if any""" + +class CmdNameColor(Command): + """ + Set or remove a special color on your name. Just an example for the + easy menu selection tree contrib. + """ + + key = "namecolor" + + def func(self): + # This is all you have to do to initialize a menu! + init_tree_selection(TEST_MENU, self.caller, + change_name_color, + start_text="Name color options:") + +def change_name_color(caller, treestr, index, selection): + """ + Changes a player's name color. + + Args: + caller (obj): Character whose name to color. + treestr (str): String for the color change menu - unused + index (int): Index of menu selection - unused + selection (str): Selection made from the name color menu - used + to determine the color the player chose. + """ + + # Store the caller's uncolored name + if not caller.db.uncolored_name: + caller.db.uncolored_name = caller.key + + # Dictionary matching color selection names to color codes + colordict = { "Red":"|511", "Pink":"|533", "Maroon":"|301", + "Orange":"|531", "Brown":"|321", "Sienna":"|420", + "Yellow":"|551", "Gold":"|540", "Dandelion":"|553", + "Green":"|141", "Lime":"|350", "Forest":"|032", + "Blue":"|115", "Cyan":"|155", "Navy":"|113", + "Purple":"|415", "Lavender":"|535", "Fuchsia":"|503"} + + # I know this probably isn't the best way to do this. It's just an example! + if selection == "Remove name color": # Player chose to remove their name color + caller.key = caller.db.uncolored_name + caller.msg("Name color removed.") + elif selection in colordict: + newcolor = colordict[selection] # Retrieve color code based on menu selection + caller.key = newcolor + caller.db.uncolored_name + "|n" # Add color code to caller's name + caller.msg(newcolor + ("Name color changed to %s!" % selection) + "|n") + From 2fcf1b3c04a5c43bdae1a6b6c27fcc49aff3375f Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 30 Oct 2017 16:21:32 -0700 Subject: [PATCH 48/68] Added unit tests for tree_select contrib --- evennia/contrib/tests.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 79a61ce65d..e2148fa0da 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1206,6 +1206,36 @@ class TestTurnBattleFunc(EvenniaTest): # Remove the script at the end turnhandler.stop() +# Test tree select + +from evennia.contrib import tree_select + +TREE_MENU_TESTSTR = """Foo +Bar +-Baz +--Baz 1 +--Baz 2 +-Qux""" + +class TestTreeSelectFunc(EvenniaTest): + + def test_tree_functions(self): + # Dash counter + self.assertTrue(tree_select.dashcount("--test") == 2) + # Is category + self.assertTrue(tree_select.is_category(TREE_MENU_TESTSTR, 1) == True) + # Parse options + self.assertTrue(tree_select.parse_opts(TREE_MENU_TESTSTR, category_index=2) == [(3, "Baz 1"), (4, "Baz 2")]) + # Index to selection + self.assertTrue(tree_select.index_to_selection(TREE_MENU_TESTSTR, 2) == "Baz") + # Go up one category + self.assertTrue(tree_select.go_up_one_category(TREE_MENU_TESTSTR, 4) == 2) + # Option list to menu options + test_optlist = tree_select.parse_opts(TREE_MENU_TESTSTR, category_index=2) + optlist_to_menu_expected_result = [{'goto': ['menunode_treeselect', {'newindex': 3}], 'key': 'Baz 1'}, + {'goto': ['menunode_treeselect', {'newindex': 4}], 'key': 'Baz 2'}, + {'goto': ['menunode_treeselect', {'newindex': 1}], 'key': ['<< Go Back', 'go back', 'back'], 'desc': 'Return to the previous menu.'}] + self.assertTrue(tree_select.optlist_to_menuoptions(TREE_MENU_TESTSTR, test_optlist, 2, True, True) == optlist_to_menu_expected_result) # Test of the unixcommand module From 8d82981a530cea42daf8f09cd190db32ae755345 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 30 Oct 2017 16:24:11 -0700 Subject: [PATCH 49/68] Add tree select to README.md --- evennia/contrib/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/evennia/contrib/README.md b/evennia/contrib/README.md index 63abb2f713..52e63b7b1a 100644 --- a/evennia/contrib/README.md +++ b/evennia/contrib/README.md @@ -50,6 +50,9 @@ things you want from here into your game folder and change them there. time to pass depending on if you are walking/running etc. * Talking NPC (Griatch 2011) - A talking NPC object that offers a menu-driven conversation tree. +* Tree Select (FlutterSprite 2017) - A simple system for creating a + branching EvMenu with selection options sourced from a single + multi-line string. * Wilderness (titeuf87 2017) - Make infinitely large wilderness areas with dynamically created locations. * UnixCommand (Vincent Le Geoff 2017) - Add commands with UNIX-style syntax. From ab46c5eefc6d5b267ae2740f2deacd9098c439e8 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Mon, 30 Oct 2017 19:16:43 -0700 Subject: [PATCH 50/68] Fix typo in documentation --- evennia/contrib/tree_select.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/tree_select.py b/evennia/contrib/tree_select.py index 56305340b3..a92cd6c2a7 100644 --- a/evennia/contrib/tree_select.py +++ b/evennia/contrib/tree_select.py @@ -52,7 +52,7 @@ Categories can be nested in other categories as well - just go another '-' deepe can do this as many times as you like. There's no hard limit to the number of categories you can go down. -For example, let's add some more options to our menu, turning 'Foo' into a category. +For example, let's add some more options to our menu, turning 'Bar' into a category. TEST_MENU = '''Foo Bar @@ -63,7 +63,7 @@ For example, let's add some more options to our menu, turning 'Foo' into a categ Baz Qux''' -Now when we call the menu, we can see that 'Foo' has become a category instead of a +Now when we call the menu, we can see that 'Bar' has become a category instead of a selectable option. _______________________________ From 831b714ad8491dea84ecbc4a880db23fe5900a79 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 5 Nov 2017 18:36:08 -0800 Subject: [PATCH 51/68] Adds health bar module Adds a versatile function that will return a given current and maximum value as a "health bar" rendered with ANSI or xterm256 background color codes. This function has many options, such as being able to specify the length of the bar, its colors (including changing color depending on how full the bar is), what text is included inside the bar and how the text is justified within it. --- evennia/contrib/health_bar.py | 103 ++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 evennia/contrib/health_bar.py diff --git a/evennia/contrib/health_bar.py b/evennia/contrib/health_bar.py new file mode 100644 index 0000000000..c3d4af1c52 --- /dev/null +++ b/evennia/contrib/health_bar.py @@ -0,0 +1,103 @@ +""" +Health Bar + +Contrib - Tim Ashley Jenkins 2017 + +The function provided in this module lets you easily display visual +bars or meters - "health bar" is merely the most obvious use for this, +though these bars are highly customizable and can be used for any sort +of appropriate data besides player health. + +Today's players may be more used to seeing statistics like health, +stamina, magic, and etc. displayed as bars rather than bare numerical +values, so using this module to present this data this way may make it +more accessible. Keep in mind, however, that players may also be using +a screen reader to connect to your game, which will not be able to +represent the colors of the bar in any way. By default, the values +represented are rendered as text inside the bar which can be read by +screen readers. + +The health bar will account for current values above the maximum or +below 0, rendering them as a completely full or empty bar with the +values displayed within. +""" + +def display_meter(cur_value, max_value, + length=30, fill_color=["R", "Y", "G"], + empty_color="B", text_color="w", + align="left", pre_text="", post_text="", + show_values=True): + """ + Represents a current and maximum value given as a "bar" rendered with + ANSI or xterm256 background colors. + + Args: + cur_value (int): Current value to display + max_value (int): Maximum value to display + + Options: + length (int): Length of meter returned, in characters + fill_color (list): List of color codes for the full portion + of the bar, sans any sort of prefix - both ANSI and xterm256 + colors are usable. When the bar is empty, colors toward the + start of the list will be chosen - when the bar is full, colors + towards the end are picked. You can adjust the 'weights' of + the changing colors by adding multiple entries of the same + color - for example, if you only want the bar to change when + it's close to empty, you could supply ['R','Y','G','G','G'] + empty_color (str): Color code for the empty portion of the bar. + text_color (str): Color code for text inside the bar. + align (str): "left", "right", or "center" - alignment of text in the bar + pre_text (str): Text to put before the numbers in the bar + post_text (str): Text to put after the numbers in the bar + show_values (bool): If true, shows the numerical values represented by + the bar. It's highly recommended you keep this on, especially if + there's no info given in pre_text or post_text, as players on screen + readers will be unable to read the graphical aspect of the bar. + """ + # Start by building the base string. + num_text = "" + if show_values: + num_text = "%i / %i" % (cur_value, max_value) + bar_base_str = pre_text + num_text + post_text + # Cut down the length of the base string if needed + if len(bar_base_str) > length: + bar_base_str = bar_base_str[:length] + # Pad and align the bar base string + if align == "right": + bar_base_str = bar_base_str.rjust(length, " ") + elif align == "center": + bar_base_str = bar_base_str.center(length, " ") + else: + bar_base_str = bar_base_str.ljust(length, " ") + + if max_value < 1: # Prevent divide by zero + max_value = 1 + if cur_value < 0: # Prevent weirdly formatted 'negative bars' + cur_value = 0 + if cur_value > max_value: # Display overfull bars correctly + cur_value = max_value + + # Now it's time to determine where to put the color codes. + percent_full = float(cur_value) / float(max_value) + split_index = round(float(length) * percent_full) + # Determine point at which to split the bar + split_index = int(split_index) + + # Separate the bar string into full and empty portions + full_portion = bar_base_str[:split_index] + empty_portion = bar_base_str[split_index:] + + # Pick which fill color to use based on how full the bar is + fillcolor_index = (float(len(fill_color)) * percent_full) + fillcolor_index = int(round(fillcolor_index)) - 1 + fillcolor_code = "|[" + fill_color[fillcolor_index] + + # Make color codes for empty bar portion and text_color + emptycolor_code = "|[" + empty_color + textcolor_code = "|" + text_color + + # Assemble the final bar + final_bar = fillcolor_code + textcolor_code + full_portion + "|n" + emptycolor_code + textcolor_code + empty_portion + "|n" + + return final_bar From f653d0d1491b0d7013e36c4334206880b2609419 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 5 Nov 2017 18:42:55 -0800 Subject: [PATCH 52/68] Add unit tests for health_bar contrib --- evennia/contrib/tests.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 79a61ce65d..4f4ac8c4bf 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -651,6 +651,15 @@ class TestGenderSub(CommandTest): char = create_object(gendersub.GenderCharacter, key="Gendered", location=self.room1) txt = "Test |p gender" self.assertEqual(gendersub._RE_GENDER_PRONOUN.sub(char._get_pronoun, txt), "Test their gender") + +# test health bar contrib + +from evennia.contrib import health_bar + +class TestHealthBar(EvenniaTest): + def test_healthbar(self): + expected_bar_str = "|[G|w |n|[B|w test24 / 200test |n" + self.assertTrue(health_bar.display_meter(24, 200, length=40, pre_text="test", post_text="test", align="center") == expected_bar_str) # test mail contrib From 2e7a9bb29e9d4080521e41bad657b090e0d3e6ad Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 12 Nov 2017 01:56:35 -0800 Subject: [PATCH 53/68] Add mention of how the callback is used --- evennia/contrib/tree_select.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/evennia/contrib/tree_select.py b/evennia/contrib/tree_select.py index a92cd6c2a7..2eab9450d7 100644 --- a/evennia/contrib/tree_select.py +++ b/evennia/contrib/tree_select.py @@ -127,7 +127,16 @@ Now we see that the Baz option has a description attached that's separate from i Bar [+] Baz: Look at this one: the best option. Qux + +Once the player makes a selection - let's say, 'Foo' - the menu will terminate and call +your specified callback with the selection, like so: + + callback(TEST_MENU, caller, 0, "Foo") +The index of the selection is given along with a string containing the selection's key. +That way, if you have two selections in the menu with the same key, you can still +differentiate between them. + And that's all there is to it! For simple branching-tree selections, using this system is much easier than manually creating EvMenu nodes. It also makes generating menus with dynamic options much easier - since the source of the menu tree is just a string, you could easily From b431025a7982c6348d6a440b92b86eb78b610fe5 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 12 Nov 2017 02:21:49 -0800 Subject: [PATCH 54/68] Fix order of args for the callback in documentation --- evennia/contrib/tree_select.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/tree_select.py b/evennia/contrib/tree_select.py index 2eab9450d7..d3befd9614 100644 --- a/evennia/contrib/tree_select.py +++ b/evennia/contrib/tree_select.py @@ -131,7 +131,7 @@ Now we see that the Baz option has a description attached that's separate from i Once the player makes a selection - let's say, 'Foo' - the menu will terminate and call your specified callback with the selection, like so: - callback(TEST_MENU, caller, 0, "Foo") + callback(caller, TEST_MENU, 0, "Foo") The index of the selection is given along with a string containing the selection's key. That way, if you have two selections in the menu with the same key, you can still @@ -171,8 +171,8 @@ def init_tree_selection(treestr, caller, callback, treestr (str): Multi-lne string representing menu options caller (obj): Player to initialize the menu for callback (callable): Function to run when a selection is made. Must take 4 args: - treestr (str): Menu tree string given above caller (obj): Caller given above + treestr (str): Menu tree string given above index (int): Index of final selection selection (str): Key of final selection From ee462dcdb35e051a9fc62cde34c39907a717d4d5 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 12 Nov 2017 11:46:59 -0800 Subject: [PATCH 55/68] Catch callback errors with logger --- evennia/contrib/tree_select.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/evennia/contrib/tree_select.py b/evennia/contrib/tree_select.py index d3befd9614..85970f58b4 100644 --- a/evennia/contrib/tree_select.py +++ b/evennia/contrib/tree_select.py @@ -158,6 +158,7 @@ character's command set. """ from evennia.utils import evmenu +from evennia.utils.logger import log_trace from evennia import Command def init_tree_selection(treestr, caller, callback, @@ -429,7 +430,10 @@ def menunode_treeselect(caller, raw_string, **kwargs): # If given index returns optlist as 'True', it's a selection. Pass to callback and end the menu. if optlist == True: selection = index_to_selection(treestr, index) - callback(caller, treestr, index, selection) + try: + callback(caller, treestr, index, selection) + except: + log_trace("Error in tree selection callback.") # Returning None, None ends the menu. return None, None From f82f629f852b3cdd44fbba56a7664b953e8a4df5 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 12 Nov 2017 11:51:53 -0800 Subject: [PATCH 56/68] Update tree_select.py --- evennia/contrib/tree_select.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/tree_select.py b/evennia/contrib/tree_select.py index 85970f58b4..8aca99ec9e 100644 --- a/evennia/contrib/tree_select.py +++ b/evennia/contrib/tree_select.py @@ -432,7 +432,7 @@ def menunode_treeselect(caller, raw_string, **kwargs): selection = index_to_selection(treestr, index) try: callback(caller, treestr, index, selection) - except: + except Exception: log_trace("Error in tree selection callback.") # Returning None, None ends the menu. From 37aef859ede721459dc48823368f343f260e67f7 Mon Sep 17 00:00:00 2001 From: FlutterSprite Date: Sun, 12 Nov 2017 11:58:23 -0800 Subject: [PATCH 57/68] Fix variable in example menu function I changed this while making unit tests and forgot to change it back. Whoops! --- evennia/contrib/tree_select.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/tree_select.py b/evennia/contrib/tree_select.py index 8aca99ec9e..5b8f038e33 100644 --- a/evennia/contrib/tree_select.py +++ b/evennia/contrib/tree_select.py @@ -496,7 +496,7 @@ class CmdNameColor(Command): def func(self): # This is all you have to do to initialize a menu! - init_tree_selection(TEST_MENU, self.caller, + init_tree_selection(NAMECOLOR_MENU, self.caller, change_name_color, start_text="Name color options:") From deac554bcc405d485d9bddabd56712d0fcf35d33 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 29 Nov 2017 19:32:50 +0100 Subject: [PATCH 58/68] Remove some spurious spaces --- evennia/contrib/tree_select.py | 140 ++++++++++++++++----------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/evennia/contrib/tree_select.py b/evennia/contrib/tree_select.py index 5b8f038e33..d2854fc83f 100644 --- a/evennia/contrib/tree_select.py +++ b/evennia/contrib/tree_select.py @@ -28,14 +28,14 @@ on a player: The player will be presented with an EvMenu, like so: ___________________________ - + Make your selection: ___________________________ - - Foo - Bar - Baz - Qux + + Foo + Bar + Baz + Qux Making a selection will pass the selection's key to the specified callback as a string along with the caller, as well as the index of the selection (the line number @@ -62,7 +62,7 @@ For example, let's add some more options to our menu, turning 'Bar' into a categ --When to walk away Baz Qux''' - + Now when we call the menu, we can see that 'Bar' has become a category instead of a selectable option. @@ -71,34 +71,34 @@ selectable option. Make your selection: _______________________________ - Foo - Bar [+] - Baz - Qux - + Foo + Bar [+] + Baz + Qux + Note the [+] next to 'Bar'. If we select 'Bar', it'll show us the option listed under it. ________________________________________________________________ Bar ________________________________________________________________ - - You've got to know [+] - << Go Back: Return to the previous menu. - + + You've got to know [+] + << Go Back: Return to the previous menu. + Just the one option, which is a category itself, and the option to go back, which will take us back to the previous menu. Let's select 'You've got to know'. ________________________________________________________________ - + You've got to know ________________________________________________________________ - - When to hold em - When to fold em - When to walk away + + When to hold em + When to fold em + When to walk away << Go Back: Return to the previous menu. - + Now we see the three options listed under it, too. We can select one of them or use 'Go Back' to return to the 'Bar' menu we were just at before. It's very simple to make a branching tree of selections! @@ -115,24 +115,24 @@ description to 'Baz' in our menu: --When to walk away Baz: Look at this one: the best option. Qux''' - + Now we see that the Baz option has a description attached that's separate from its key: _______________________________________________________________ Make your selection: _______________________________________________________________ - - Foo - Bar [+] - Baz: Look at this one: the best option. - Qux + + Foo + Bar [+] + Baz: Look at this one: the best option. + Qux Once the player makes a selection - let's say, 'Foo' - the menu will terminate and call your specified callback with the selection, like so: callback(caller, TEST_MENU, 0, "Foo") - + The index of the selection is given along with a string containing the selection's key. That way, if you have two selections in the menu with the same key, you can still differentiate between them. @@ -167,7 +167,7 @@ def init_tree_selection(treestr, caller, callback, start_text="Make your selection:"): """ Prompts a player to select an option from a menu tree given as a multi-line string. - + Args: treestr (str): Multi-lne string representing menu options caller (obj): Player to initialize the menu for @@ -176,33 +176,33 @@ def init_tree_selection(treestr, caller, callback, treestr (str): Menu tree string given above index (int): Index of final selection selection (str): Key of final selection - + Options: index (int or None): Index to start the menu at, or None for top level mark_category (bool): If True, marks categories with a [+] symbol in the menu go_back (bool): If True, present an option to go back to previous categories start_text (str): Text to display at the top level of the menu cmd_on_exit(str): Command to enter when the menu exits - 'look' by default - - + + Notes: This function will initialize an instance of EvMenu with options generated dynamically from the source string, and passes the menu user's selection to a function of your choosing. The EvMenu is made of a single, repeating node, which will call itself over and over at different levels of the menu tree as categories are selected. - + Once a non-category selection is made, the user's selection will be passed to the given callable, both as a string and as an index number. The index is given to ensure every selection has a unique identifier, so that selections with the same key in different categories can be distinguished between. - + The menus called by this function are not persistent and cannot perform complicated tasks like prompt for arbitrary input or jump multiple category levels at once - you'll have to use EvMenu itself if you want to take full advantage of its features. - """ - + """ + # Pass kwargs to store data needed in the menu kwargs = { "index":index, @@ -212,7 +212,7 @@ def init_tree_selection(treestr, caller, callback, "callback":callback, "start_text":start_text } - + # Initialize menu of selections evmenu.EvMenu(caller, "evennia.contrib.tree_select", startnode="menunode_treeselect", startnode_input=None, cmd_on_exit=cmd_on_exit, **kwargs) @@ -221,10 +221,10 @@ def dashcount(entry): """ Counts the number of dashes at the beginning of a string. This is needed to determine the depth of options in categories. - + Args: entry (str): String to count the dashes at the start of - + Returns: dashes (int): Number of dashes at the start """ @@ -240,11 +240,11 @@ def is_category(treestr, index): """ Determines whether an option in a tree string is a category by whether or not there are additional options below it. - + Args: treestr (str): Multi-line string representing menu options index (int): Which line of the string to test - + Returns: is_category (bool): Whether the option is a category """ @@ -262,11 +262,11 @@ def parse_opts(treestr, category_index=None): the menu. If category_index corresponds to a category, returns a list of options under that category. If category_index corresponds to an option that is not a category, it's a selection and returns True. - + Args: treestr (str): Multi-line string representing menu options category_index (int): Index of category or None for top level - + Returns: kept_opts (list or True): Either a list of options in the selected category or True if a selection was made @@ -274,7 +274,7 @@ def parse_opts(treestr, category_index=None): dash_depth = 0 opt_list = treestr.split('\n') kept_opts = [] - + # If a category index is given if category_index != None: # If given index is not a category, it's a selection - return True. @@ -284,7 +284,7 @@ def parse_opts(treestr, category_index=None): dash_depth = dashcount(opt_list[category_index]) + 1 # Delete everything before the category index opt_list = opt_list [category_index+1:] - + # Keep every option (referenced by index) at the appropriate depth cur_index = 0 for option in opt_list: @@ -298,20 +298,20 @@ def parse_opts(treestr, category_index=None): return kept_opts cur_index += 1 return kept_opts - + def index_to_selection(treestr, index, desc=False): """ Given a menu tree string and an index, returns the corresponding selection's name as a string. If 'desc' is set to True, will return the selection's description as a string instead. - + Args: treestr (str): Multi-line string representing menu options index (int): Index to convert to selection key or description - + Options: desc (bool): If true, returns description instead of key - + Returns: selection (str): Selection key or description if 'desc' is set """ @@ -332,16 +332,16 @@ def index_to_selection(treestr, index, desc=False): return selection[0] else: return selection[1] - + def go_up_one_category(treestr, index): """ Given a menu tree string and an index, returns the category that the given option belongs to. Used for the 'go back' option. - + Args: treestr (str): Multi-line string representing menu options index (int): Index to determine the parent category of - + Returns: parent_category (int): Index of parent category """ @@ -350,7 +350,7 @@ def go_up_one_category(treestr, index): dash_level = dashcount(opt_list[index]) # Delete everything after the current index opt_list = opt_list[:index+1] - + # If there's no dash, return 'None' to return to base menu if dash_level == 0: @@ -361,25 +361,25 @@ def go_up_one_category(treestr, index): if dashcount(selection) == dash_level - 1: return current_index current_index -= 1 - + def optlist_to_menuoptions(treestr, optlist, index, mark_category, go_back): """ Takes a list of options processed by parse_opts and turns it into a list/dictionary of menu options for use in menunode_treeselect. - + Args: treestr (str): Multi-line string representing menu options optlist (list): List of options to convert to EvMenu's option format index (int): Index of current category mark_category (bool): Whether or not to mark categories with [+] go_back (bool): Whether or not to add an option to go back in the menu - + Returns: menuoptions (list of dicts): List of menu options formatted for use in EvMenu, each passing a different "newindex" kwarg that changes the menu level or makes a selection """ - + menuoptions = [] cur_index = 0 for option in optlist: @@ -410,12 +410,12 @@ def optlist_to_menuoptions(treestr, optlist, index, mark_category, go_back): def menunode_treeselect(caller, raw_string, **kwargs): """ This is the repeating menu node that handles the tree selection. - """ - + """ + # If 'newindex' is in the kwargs, change the stored index. if "newindex" in kwargs: caller.ndb._menutree.index = kwargs["newindex"] - + # Retrieve menu info index = caller.ndb._menutree.index mark_category = caller.ndb._menutree.mark_category @@ -423,10 +423,10 @@ def menunode_treeselect(caller, raw_string, **kwargs): treestr = caller.ndb._menutree.treestr callback = caller.ndb._menutree.callback start_text = caller.ndb._menutree.start_text - + # List of options if index is 'None' or category, or 'True' if a selection optlist = parse_opts(treestr, category_index=index) - + # If given index returns optlist as 'True', it's a selection. Pass to callback and end the menu. if optlist == True: selection = index_to_selection(treestr, index) @@ -434,10 +434,10 @@ def menunode_treeselect(caller, raw_string, **kwargs): callback(caller, treestr, index, selection) except Exception: log_trace("Error in tree selection callback.") - + # Returning None, None ends the menu. return None, None - + # Otherwise, convert optlist to a list of menu options. else: options = optlist_to_menuoptions(treestr, optlist, index, mark_category, go_back) @@ -485,7 +485,7 @@ NAMECOLOR_MENU = """Set name color: Choose a color for your name! --Lavender: |535Set your name to Lavender|n --Fuchsia: |503Set your name to Fuchsia|n Remove name color: Remove your name color, if any""" - + class CmdNameColor(Command): """ Set or remove a special color on your name. Just an example for the @@ -503,7 +503,7 @@ class CmdNameColor(Command): def change_name_color(caller, treestr, index, selection): """ Changes a player's name color. - + Args: caller (obj): Character whose name to color. treestr (str): String for the color change menu - unused @@ -511,11 +511,11 @@ def change_name_color(caller, treestr, index, selection): selection (str): Selection made from the name color menu - used to determine the color the player chose. """ - + # Store the caller's uncolored name if not caller.db.uncolored_name: caller.db.uncolored_name = caller.key - + # Dictionary matching color selection names to color codes colordict = { "Red":"|511", "Pink":"|533", "Maroon":"|301", "Orange":"|531", "Brown":"|321", "Sienna":"|420", @@ -523,7 +523,7 @@ def change_name_color(caller, treestr, index, selection): "Green":"|141", "Lime":"|350", "Forest":"|032", "Blue":"|115", "Cyan":"|155", "Navy":"|113", "Purple":"|415", "Lavender":"|535", "Fuchsia":"|503"} - + # I know this probably isn't the best way to do this. It's just an example! if selection == "Remove name color": # Player chose to remove their name color caller.key = caller.db.uncolored_name From fa52cf917303f99439320dc855ed67abbf883171 Mon Sep 17 00:00:00 2001 From: Tehom Date: Fri, 8 Dec 2017 02:59:29 -0500 Subject: [PATCH 59/68] Make at_say more flexible by not ignoring parameters passed --- evennia/objects/objects.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index e0fd0dace4..e0f337d3ee 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1685,10 +1685,8 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): msg_type = 'whisper' msg_self = '{self} whisper to {all_receivers}, "{speech}"' if msg_self is True else msg_self msg_receivers = '{object} whispers: "{speech}"' - msg_location = None else: msg_self = '{self} say, "{speech}"' if msg_self is True else msg_self - msg_receivers = None msg_location = msg_location or '{object} says, "{speech}"' custom_mapping = kwargs.get('mapping', {}) @@ -1733,9 +1731,14 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): "receiver": None, "speech": message} location_mapping.update(custom_mapping) + exclude = [] + if msg_self: + exclude.append(self) + if receivers: + exclude.extend(receivers) self.location.msg_contents(text=(msg_location, {"type": msg_type}), from_obj=self, - exclude=(self, ) if msg_self else None, + exclude=exclude, mapping=location_mapping) From 391132bed21390a2220785217baadb8a63e68791 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 9 Dec 2017 01:13:02 +0100 Subject: [PATCH 60/68] Change dockerfile entrypoint to launch evennia server, more suitable for docker-compose setups. Add websocket proxy envvar --- Dockerfile | 12 ++++++------ bin/unix/evennia-docker-start.sh | 13 +++++++++++++ evennia/settings_default.py | 7 ++++++- evennia/web/utils/general_context.py | 7 ++++++- 4 files changed, 31 insertions(+), 8 deletions(-) create mode 100644 bin/unix/evennia-docker-start.sh diff --git a/Dockerfile b/Dockerfile index 27af6b2a3a..4ca00d254b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,11 +5,11 @@ # install `docker` (http://docker.com) # # Usage: -# cd to a folder where you want your game data to be (or where it already is). +# cd to a folder where you want your game data to be (or where it already is). # # docker run -it -p 4000:4000 -p 4001:4001 -p 4005:4005 -v $PWD:/usr/src/game evennia/evennia -# -# (If your OS does not support $PWD, replace it with the full path to your current +# +# (If your OS does not support $PWD, replace it with the full path to your current # folder). # # You will end up in a shell where the `evennia` command is available. From here you @@ -30,10 +30,10 @@ RUN apk update && apk add python py-pip python-dev py-setuptools gcc musl-dev jp ADD . /usr/src/evennia # install dependencies -RUN pip install -e /usr/src/evennia --trusted-host pypi.python.org +RUN pip install --upgrade pip && pip install /usr/src/evennia --trusted-host pypi.python.org # add the game source when rebuilding a new docker image from inside -# a game dir +# a game dir ONBUILD ADD . /usr/src/game # make the game source hierarchy persistent with a named volume. @@ -48,7 +48,7 @@ WORKDIR /usr/src/game ENV PS1 "evennia|docker \w $ " # startup a shell when we start the container -ENTRYPOINT ["bash"] +ENTRYPOINT bash -c "source /usr/src/evennia/bin/unix/evennia-docker-start.sh" # expose the telnet, webserver and websocket client ports EXPOSE 4000 4001 4005 diff --git a/bin/unix/evennia-docker-start.sh b/bin/unix/evennia-docker-start.sh new file mode 100644 index 0000000000..270f6ec627 --- /dev/null +++ b/bin/unix/evennia-docker-start.sh @@ -0,0 +1,13 @@ +#! /bin/bash + +# called by the Dockerfile to start the server in docker mode + +# remove leftover .pid files (such as from when dropping the container) +rm /usr/src/game/server/*.pid >& /dev/null || true + +# start evennia server; log to server.log but also output to stdout so it can +# be viewed with docker-compose logs +exec 3>&1; evennia start 2>&1 1>&3 | tee /usr/src/game/server/logs/server.log; exec 3>&- + +# start a shell to keep the container running +bash diff --git a/evennia/settings_default.py b/evennia/settings_default.py index b1e2a7a79e..fd213d333d 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -83,7 +83,12 @@ WEBCLIENT_ENABLED = True # default webclient will use this and only use the ajax version if the browser # is too old to support websockets. Requires WEBCLIENT_ENABLED. WEBSOCKET_CLIENT_ENABLED = True -# Server-side websocket port to open for the webclient. +# Server-side websocket port to open for the webclient. Note that this value will +# be dynamically encoded in the webclient html page to allow the webclient to call +# home. If the external encoded value needs to be different than this, due to +# working through a proxy or docker port-remapping, the environment variable +# WEBCLIENT_CLIENT_PROXY_PORT can be used to override this port only for the +# front-facing client's sake. WEBSOCKET_CLIENT_PORT = 4005 # Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6. WEBSOCKET_CLIENT_INTERFACE = '0.0.0.0' diff --git a/evennia/web/utils/general_context.py b/evennia/web/utils/general_context.py index 27edd79c86..44bc8b3cb3 100644 --- a/evennia/web/utils/general_context.py +++ b/evennia/web/utils/general_context.py @@ -6,6 +6,7 @@ # tuple. # +import os from django.conf import settings from evennia.utils.utils import get_evennia_version @@ -52,7 +53,11 @@ def set_webclient_settings(): global WEBCLIENT_ENABLED, WEBSOCKET_CLIENT_ENABLED, WEBSOCKET_PORT, WEBSOCKET_URL WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED WEBSOCKET_CLIENT_ENABLED = settings.WEBSOCKET_CLIENT_ENABLED - WEBSOCKET_PORT = settings.WEBSOCKET_CLIENT_PORT + # if we are working through a proxy or uses docker port-remapping, the webclient port encoded + # in the webclient should be different than the one the server expects. Use the environment + # variable WEBSOCKET_CLIENT_PROXY_PORT if this is the case. + WEBSOCKET_PORT = int(os.environ.get("WEBSOCKET_CLIENT_PROXY_PORT", settings.WEBSOCKET_CLIENT_PORT)) + # this is determined dynamically by the client and is less of an issue WEBSOCKET_URL = settings.WEBSOCKET_CLIENT_URL set_webclient_settings() From 7e1900e617ed58b13f431b7ce66a6f4d465471b3 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 10 Dec 2017 10:26:20 +0100 Subject: [PATCH 61/68] fix unittests --- evennia/web/utils/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/web/utils/tests.py b/evennia/web/utils/tests.py index b2d42891ae..e2b28c3510 100644 --- a/evennia/web/utils/tests.py +++ b/evennia/web/utils/tests.py @@ -51,9 +51,9 @@ class TestGeneralContext(TestCase): mock_settings.WEBCLIENT_ENABLED = "webclient" mock_settings.WEBSOCKET_CLIENT_URL = "websocket_url" mock_settings.WEBSOCKET_CLIENT_ENABLED = "websocket_client" - mock_settings.WEBSOCKET_CLIENT_PORT = "websocket_port" + mock_settings.WEBSOCKET_CLIENT_PORT = 5000 general_context.set_webclient_settings() self.assertEqual(general_context.WEBCLIENT_ENABLED, "webclient") self.assertEqual(general_context.WEBSOCKET_URL, "websocket_url") self.assertEqual(general_context.WEBSOCKET_CLIENT_ENABLED, "websocket_client") - self.assertEqual(general_context.WEBSOCKET_PORT, "websocket_port") + self.assertEqual(general_context.WEBSOCKET_PORT, 5000) From 5aa9dfc24c9dac902363d511268444232059a3a2 Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Tue, 12 Dec 2017 18:56:36 +0100 Subject: [PATCH 62/68] Add a setting to change telnet default encoding --- evennia/server/portal/telnet.py | 5 +++-- evennia/server/session.py | 7 ++++++- evennia/settings_default.py | 6 ++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index 4112d85e2a..86199ae4ac 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -24,7 +24,7 @@ _RE_LEND = re.compile(r"\n$|\r$|\r\n$|\r\x00$|", re.MULTILINE) _RE_LINEBREAK = re.compile(r"\n\r|\r\n|\n|\r", re.DOTALL + re.MULTILINE) _RE_SCREENREADER_REGEX = re.compile(r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE) _IDLE_COMMAND = settings.IDLE_COMMAND + "\n" - +_TELNET_ENCODING = settings.TELNET_ENCODING class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): """ @@ -49,7 +49,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): # this number is counted down for every handshake that completes. # when it reaches 0 the portal/server syncs their data self.handshakes = 8 # suppress-go-ahead, naws, ttype, mccp, mssp, msdp, gmcp, mxp - self.init_session(self.protocol_name, client_address, self.factory.sessionhandler) + self.init_session(self.protocol_name, client_address, self.factory.sessionhandler, + override_flags={"ENCODING": _TELNET_ENCODING}) # suppress go-ahead self.sga = suppress_ga.SuppressGA(self) diff --git a/evennia/server/session.py b/evennia/server/session.py index 70be0708d7..dc816ee59b 100644 --- a/evennia/server/session.py +++ b/evennia/server/session.py @@ -41,7 +41,7 @@ class Session(object): 'conn_time', 'cmd_last', 'cmd_last_visible', 'cmd_total', 'protocol_flags', 'server_data', "cmdset_storage_string") - def init_session(self, protocol_key, address, sessionhandler): + def init_session(self, protocol_key, address, sessionhandler, override_flags=None): """ Initialize the Session. This should be called by the protocol when a new session is established. @@ -52,6 +52,7 @@ class Session(object): address (str): Client address. sessionhandler (SessionHandler): Reference to the main sessionhandler instance. + override_flags (optional, dict): a dictionary of protocol flags to override. """ # This is currently 'telnet', 'ssh', 'ssl' or 'web' @@ -87,6 +88,10 @@ class Session(object): "INPUTDEBUG": False, "RAW": False, "NOCOLOR": False} + + if override_flags: + self.protocol_flags.update(override_flags) + self.server_data = {} # map of input data to session methods diff --git a/evennia/settings_default.py b/evennia/settings_default.py index fd213d333d..bb8e07fe44 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -166,6 +166,12 @@ IDLE_TIMEOUT = -1 # command-name is given here; this is because the webclient needs a default # to send to avoid proxy timeouts. IDLE_COMMAND = "idle" +# The encoding (character set) specific to Telnet. This will not influence +# other encoding settings: namely, the webclient, the website, the +# database encoding will remain (utf-8 by default). This setting only +# affects the telnet encoding and will be overridden by user settings +# (through one of their client's supported protocol or their account options). +TELNET_ENCODING = "utf-8" # The set of encodings tried. An Account object may set an attribute "encoding" on # itself to match the client used. If not set, or wrong encoding is # given, this list is tried, in order, aborting on the first match. From f3c909ed6aef62a60c130d9d63b0cb9977f69258 Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Tue, 12 Dec 2017 19:46:28 +0100 Subject: [PATCH 63/68] Remove TELNET_ENCODING and set ENCODINGS[0] --- evennia/server/portal/telnet.py | 2 +- evennia/settings_default.py | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index 86199ae4ac..9d7b31929d 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -24,7 +24,7 @@ _RE_LEND = re.compile(r"\n$|\r$|\r\n$|\r\x00$|", re.MULTILINE) _RE_LINEBREAK = re.compile(r"\n\r|\r\n|\n|\r", re.DOTALL + re.MULTILINE) _RE_SCREENREADER_REGEX = re.compile(r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE) _IDLE_COMMAND = settings.IDLE_COMMAND + "\n" -_TELNET_ENCODING = settings.TELNET_ENCODING +_TELNET_ENCODING = settings.ENCODINGS[0] class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): """ diff --git a/evennia/settings_default.py b/evennia/settings_default.py index bb8e07fe44..5469fd46db 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -166,17 +166,12 @@ IDLE_TIMEOUT = -1 # command-name is given here; this is because the webclient needs a default # to send to avoid proxy timeouts. IDLE_COMMAND = "idle" -# The encoding (character set) specific to Telnet. This will not influence -# other encoding settings: namely, the webclient, the website, the -# database encoding will remain (utf-8 by default). This setting only -# affects the telnet encoding and will be overridden by user settings -# (through one of their client's supported protocol or their account options). -TELNET_ENCODING = "utf-8" # The set of encodings tried. An Account object may set an attribute "encoding" on # itself to match the client used. If not set, or wrong encoding is # given, this list is tried, in order, aborting on the first match. # Add sets for languages/regions your accounts are likely to use. # (see http://en.wikipedia.org/wiki/Character_encoding) +# Telnet default encoding, unless specified by the client, will be ENCODINGS[0]. ENCODINGS = ["utf-8", "latin-1", "ISO-8859-1"] # Regular expression applied to all output to a given session in order # to strip away characters (usually various forms of decorations) for the benefit From 94e271c6931cf7c603dc06954f5b9b8da7d56188 Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Tue, 12 Dec 2017 20:51:16 +0100 Subject: [PATCH 64/68] Simplify telnet edefault encoding --- evennia/server/portal/telnet.py | 6 +++--- evennia/server/session.py | 8 +------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index 9d7b31929d..08267f0ee9 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -24,7 +24,6 @@ _RE_LEND = re.compile(r"\n$|\r$|\r\n$|\r\x00$|", re.MULTILINE) _RE_LINEBREAK = re.compile(r"\n\r|\r\n|\n|\r", re.DOTALL + re.MULTILINE) _RE_SCREENREADER_REGEX = re.compile(r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE) _IDLE_COMMAND = settings.IDLE_COMMAND + "\n" -_TELNET_ENCODING = settings.ENCODINGS[0] class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): """ @@ -49,8 +48,9 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): # this number is counted down for every handshake that completes. # when it reaches 0 the portal/server syncs their data self.handshakes = 8 # suppress-go-ahead, naws, ttype, mccp, mssp, msdp, gmcp, mxp - self.init_session(self.protocol_name, client_address, self.factory.sessionhandler, - override_flags={"ENCODING": _TELNET_ENCODING}) + self.init_session(self.protocol_name, client_address, self.factory.sessionhandler) + # change encoding to ENCODINGS[0] which reflects Telnet default encoding + self.protocol_flags["ENCODING"] = settings.ENCODINGS[0] # suppress go-ahead self.sga = suppress_ga.SuppressGA(self) diff --git a/evennia/server/session.py b/evennia/server/session.py index dc816ee59b..96b68662a5 100644 --- a/evennia/server/session.py +++ b/evennia/server/session.py @@ -7,7 +7,6 @@ from builtins import object import time - #------------------------------------------------------------ # Server Session #------------------------------------------------------------ @@ -41,7 +40,7 @@ class Session(object): 'conn_time', 'cmd_last', 'cmd_last_visible', 'cmd_total', 'protocol_flags', 'server_data', "cmdset_storage_string") - def init_session(self, protocol_key, address, sessionhandler, override_flags=None): + def init_session(self, protocol_key, address, sessionhandler): """ Initialize the Session. This should be called by the protocol when a new session is established. @@ -52,7 +51,6 @@ class Session(object): address (str): Client address. sessionhandler (SessionHandler): Reference to the main sessionhandler instance. - override_flags (optional, dict): a dictionary of protocol flags to override. """ # This is currently 'telnet', 'ssh', 'ssl' or 'web' @@ -88,10 +86,6 @@ class Session(object): "INPUTDEBUG": False, "RAW": False, "NOCOLOR": False} - - if override_flags: - self.protocol_flags.update(override_flags) - self.server_data = {} # map of input data to session methods From 7fb30c5bc3a4991fa5b1a701051f21822f5f6d68 Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Tue, 12 Dec 2017 20:59:55 +0100 Subject: [PATCH 65/68] Add a little check in case ENCODINGS is empty --- evennia/server/portal/telnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index 08267f0ee9..dd07512b70 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -50,7 +50,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): self.handshakes = 8 # suppress-go-ahead, naws, ttype, mccp, mssp, msdp, gmcp, mxp self.init_session(self.protocol_name, client_address, self.factory.sessionhandler) # change encoding to ENCODINGS[0] which reflects Telnet default encoding - self.protocol_flags["ENCODING"] = settings.ENCODINGS[0] + self.protocol_flags["ENCODING"] = settings.ENCODINGS[0] if settings.ENCODINGS else 'utf-8' # suppress go-ahead self.sga = suppress_ga.SuppressGA(self) From af1057d190c4e6d91db07c3c94a0fe21b409329b Mon Sep 17 00:00:00 2001 From: Robert Bost Date: Tue, 12 Dec 2017 19:29:16 -0500 Subject: [PATCH 66/68] Update websocket URL so proxy port can be utilized. Resolves #1421. --- evennia/settings_default.py | 6 +++--- evennia/web/webclient/templates/webclient/base.html | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/evennia/settings_default.py b/evennia/settings_default.py index b1e2a7a79e..9a0ec3ffa3 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -89,9 +89,9 @@ WEBSOCKET_CLIENT_PORT = 4005 WEBSOCKET_CLIENT_INTERFACE = '0.0.0.0' # Actual URL for webclient component to reach the websocket. You only need # to set this if you know you need it, like using some sort of proxy setup. -# If given it must be on the form "ws://hostname" (WEBSOCKET_CLIENT_PORT will -# be automatically appended). If left at None, the client will itself -# figure out this url based on the server's hostname. +# If given it must be on the form "ws[s]://hostname[:port]". If left at None, +# the client will itself figure out this url based on the server's hostname. +# e.g. ws://external.example.com or wss://external.example.com:443 WEBSOCKET_CLIENT_URL = None # This determine's whether Evennia's custom admin page is used, or if the # standard Django admin is used. diff --git a/evennia/web/webclient/templates/webclient/base.html b/evennia/web/webclient/templates/webclient/base.html index 373ff0f357..30a68c498f 100644 --- a/evennia/web/webclient/templates/webclient/base.html +++ b/evennia/web/webclient/templates/webclient/base.html @@ -44,7 +44,7 @@ JQuery available. {% endif %} {% if websocket_url %} - var wsurl = "{{websocket_url}}:{{websocket_port}}"; + var wsurl = "{{websocket_url}}"; {% else %} var wsurl = "ws://" + this.location.hostname + ":{{websocket_port}}"; {% endif %} From 56d9887825a1a113ea82e987679f3a4b05065698 Mon Sep 17 00:00:00 2001 From: Tehom Date: Sun, 17 Dec 2017 18:53:41 -0500 Subject: [PATCH 67/68] Allow other typeclasses to have their Attributes set via command. --- evennia/commands/default/building.py | 42 +++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index f4a001e6d2..1e544748ff 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1455,6 +1455,11 @@ class CmdSetAttribute(ObjManipCommand): Switch: edit: Open the line editor (string values only) + script: If we're trying to set an attribute on a script + channel: If we're trying to set an attribute on a channel + account: If we're trying to set an attribute on an account + (room, exit, char/character may all be used as well for global + searches) Sets attributes on objects. The second form clears a previously set attribute while the last form @@ -1555,6 +1560,38 @@ class CmdSetAttribute(ObjManipCommand): # start the editor EvEditor(self.caller, load, save, key="%s/%s" % (obj, attr)) + def search_for_obj(self, objname): + """ + Searches for an object matching objname. The object may be of different typeclasses. + Args: + objname: Name of the object we're looking for + + Returns: + A typeclassed object, or None if nothing is found. + """ + from evennia.utils.utils import variable_from_module + _AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) + caller = self.caller + if objname.startswith('*') or "account" in self.switches: + found_obj = caller.search_account(objname.lstrip('*')) + elif "script" in self.switches: + found_obj = _AT_SEARCH_RESULT(search.search_script(objname), caller) + elif "channel" in self.switches: + found_obj = _AT_SEARCH_RESULT(search.search_channel(objname), caller) + else: + global_search = True + if "char" in self.switches or "character" in self.switches: + typeclass = settings.BASE_CHARACTER_TYPECLASS + elif "room" in self.switches: + typeclass = settings.BASE_ROOM_TYPECLASS + elif "exit" in self.switches: + typeclass = settings.BASE_EXIT_TYPECLASS + else: + global_search = False + typeclass = None + found_obj = caller.search(objname, global_search=global_search, typeclass=typeclass) + return found_obj + def func(self): """Implement the set attribute - a limited form of @py.""" @@ -1568,10 +1605,7 @@ class CmdSetAttribute(ObjManipCommand): objname = self.lhs_objattr[0]['name'] attrs = self.lhs_objattr[0]['attrs'] - if objname.startswith('*'): - obj = caller.search_account(objname.lstrip('*')) - else: - obj = caller.search(objname) + obj = self.search_for_obj(objname) if not obj: return From 33645df6dda6990f5bdd78c3ab7aea87a99ef5e7 Mon Sep 17 00:00:00 2001 From: Tehom Date: Tue, 19 Dec 2017 00:42:23 -0500 Subject: [PATCH 68/68] Try to clarify help file. --- evennia/commands/default/building.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 1e544748ff..5076170957 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1458,8 +1458,10 @@ class CmdSetAttribute(ObjManipCommand): script: If we're trying to set an attribute on a script channel: If we're trying to set an attribute on a channel account: If we're trying to set an attribute on an account - (room, exit, char/character may all be used as well for global - searches) + room: Setting an attribute on a room (global search) + exit: Setting an attribute on an exit (global search) + char: Setting an attribute on a character (global search) + character: Alias for char, as above. Sets attributes on objects. The second form clears a previously set attribute while the last form