From 55d2a67cc6aa04b2f261bfc8fd2c100f5ae1415f Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 27 Jan 2023 22:23:41 +0100 Subject: [PATCH] Add fly/dive command to XYZGrid to simulate full 3D movement --- CHANGELOG.md | 10 +- evennia/contrib/grid/xyzgrid/README.md | 108 +++++++++++++++--- evennia/contrib/grid/xyzgrid/commands.py | 80 ++++++++++++- evennia/contrib/grid/xyzgrid/tests.py | 138 +++++++++++++++++++---- evennia/contrib/grid/xyzgrid/xyzgrid.py | 2 +- evennia/contrib/grid/xyzgrid/xyzroom.py | 7 +- evennia/server/serversession.py | 2 +- 7 files changed, 304 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e4543cbb2..04f81095f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,15 +2,19 @@ ## Main -- Bug fix: $an() inlinefunc didn't understand to use 'an' words starting with a +- Feature: Add `fly/dive` commands to `XYZGrid` contrib to showcase treating its + Z-axis as a full 3D grid. Also fixed minor bug in `XYZGrid` contrib when using + a Z axis named using an integer rather than a string. +- Bug fix: `$an()` inlinefunc didn't understand to use 'an' words starting with a capital vowel +- Bug fix: Another case of the 'duplicate Discord bot connections' bug ## Evennia 1.1.1 - Bug fix: Better handler malformed alias-regex given to nickhandler. A regex-relevant character in a channel alias could cause server to not restart. -- Add `attr` keyword to `create_channel`. This allows setting attributes on - channels at creation, also from `DEFAULT_CHANNELS` definitions. +- Feature: Add `attr` keyword to `create_channel`. This allows setting + attributes on channels at creation, also from `DEFAULT_CHANNELS` definitions. ## Evennia 1.1.0 Jan 7, 2023 diff --git a/evennia/contrib/grid/xyzgrid/README.md b/evennia/contrib/grid/xyzgrid/README.md index 2fb4f0002d..73871d5434 100644 --- a/evennia/contrib/grid/xyzgrid/README.md +++ b/evennia/contrib/grid/xyzgrid/README.md @@ -310,24 +310,102 @@ In the following sections we'll discuss each component in turn. ### The Zcoord -Each XYMap on the grid has a Z-coordinate which usually can be treated just as the -name of the map. This is a string that must be unique across the entire grid. -It is added as the key 'zcoord' to `XYMAP_DATA`. - -Actual 3D movement is usually impractical in a text-based game, so all movements -and pathfinding etc happens within each XYMap (up/down is 'faked' within the XY -plane). Even for the most hardcore of sci-fi space game, moving on a 2D plane -usually makes it much easier for players than to attempt to have them visualize -actual 3D movements. - -If you really wanted an actual 3D coordinate system, you could theoretically -make all maps the same size and name them `0`, `1`, `2` etc. But even then, you -could not freely move up/down between every point (a special Transitional Node -is required as outlined below). Also pathfinding will only work per-XYMap. +Each XYMap on the grid has a Z-coordinate which usually can be treated just as +the name of the map. The Z-coordinate can be either a string or an integer, and must +be unique across the entire grid. It is added as the key 'zcoord' to `XYMAP_DATA`. Most users will want to just treat each map as a location, and name the "Z-coordinate" things like `Dungeon of Doom`, `The ice queen's palace` or `City -of Blackhaven`. +of Blackhaven`. But you could also name it -1, 0, 1, 2, 3 if you wanted. + +Pathfinding happens only within each XYMap (up/down is normally 'faked' by moving +sideways to a new area of the XY plane). + +#### A true 3D map + +Even for the most hardcore of sci-fi space game, consider sticking to 2D +movement. It's hard enough for players to visualize a 3D volume with graphics. +In text it's even harder. + +That said, if you want to set up a true X, Y, Z 3D coordinate system (where +you can move up/down from every point), you can do that too. + +This contrib provides an example command `commands.CmdFlyAndDive` that provides the player +with the ability to use `fly` and `dive` to move straight up/down between Z +coordinates. Just add it (or its cmdset `commands.XYZGridFlyDiveCmdSet`) to your +Character cmdset and reload to try it out. + +For the fly/dive to work you need to build your grid as a 'stack' of XY-grid maps +and name them by their Z-coordinate as an integer. The fly/dive actions will +only work if there is actually a matching room directly above/below. + +> Note that since pathfinding only works within each XYmap, the player will not +> be able to include fly/dive in their autowalking - this is always a manual +> action. + +As an example, let's assume coordinate `(1, 1, -3)` +is the bottom of a deep well leading up to the surface (at level 0) + +``` +LEVEL_MINUS_3 = r""" ++ 0 1 + +1 # + | +0 #-# + ++ 0 1 +""" + +LEVEL_MINUS_2 = r""" ++ 0 1 + +1 # + +0 + ++ 0 1 +""" + +LEVEL_MINUS_1 = r""" ++ 0 1 + +1 # + +0 + ++ 0 1 +""" + +LEVEL_0 = r""" ++ 0 1 + +1 #-# + |x| +0 #-# + ++ 0 1 +""" + +XYMAP_DATA_LIST = [ + {"zcoord": -3, "map": LEVEL_MINUS_3}, + {"zcoord": -2, "map": LEVEL_MINUS_2}, + {"zcoord": -1, "map": LEVEL_MINUS_1}, + {"zcoord": 0, "map": LEVEL_0}, +] +``` + +In this example, if we arrive to the bottom of the well at `(1, 1, -3)` we +`fly` straight up three levels until we arrive at `(1, 1, 0)`, at the corner +of some sort of open field. + +We can dive down from `(1, 1, 0)`. In the default implementation you must `dive` 3 times +to get to the bottom. If you wanted you could tweak the command so you +automatically fall to the bottom and take damage etc. + +We can't fly/dive up/down from any other XY positions because there are no open rooms at the +adjacent Z coordinates. + ### Map String diff --git a/evennia/contrib/grid/xyzgrid/commands.py b/evennia/contrib/grid/xyzgrid/commands.py index 3ff1fe25dd..17e60b3e48 100644 --- a/evennia/contrib/grid/xyzgrid/commands.py +++ b/evennia/contrib/grid/xyzgrid/commands.py @@ -10,7 +10,6 @@ the commands with XYZ-aware equivalents. from collections import namedtuple from django.conf import settings - from evennia import CmdSet, InterruptCommand, default_cmds from evennia.commands.default import building from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid @@ -507,3 +506,82 @@ class XYZGridCmdSet(CmdSet): self.add(CmdXYZOpen()) self.add(CmdGoto()) self.add(CmdMap()) + + +# Optional fly/dive commands to move between maps (enable +# full 3D-grid movements) + + +class CmdFlyAndDive(COMMAND_DEFAULT_CLASS): + """ + Fly or Dive up and down. + + Usage: + fly + dive + + Will fly up one room or dive down one room at your current position. If + there is no room above/below you, your movement will fail. + + """ + + key = "fly or dive" + aliases = ("fly", "dive") + + def func(self): + caller = self.caller + + action = self.cmdname + + try: + xyz_start = caller.location.xyz + except AttributeError: + caller.msg(f"You cannot {action} here.") + return + try: + zcoord = int(xyz_start[2]) + except ValueError: + caller.msg(f"You cannot {action} here.") + return + + if action == "fly": + diff = 1 + direction = "upwards" + from_direction = "below" + error_message = "Can't fly here - you'd hit your head." + elif action == "dive": + diff = -1 + direction = "downwards" + from_direction = "above" + error_message = "Can't dive here - you'd just fall flat on the ground." + else: + caller.msg("You must decide if you want to |wfly|n up or |wdive|n down.") + return + + target_coord = (str(xyz_start[0]), str(xyz_start[1]), zcoord + diff) + try: + target = XYZRoom.objects.get_xyz(xyz=(target_coord)) + except XYZRoom.DoesNotExist: + # no available room above/below to fly/dive to + caller.msg(error_message) + return + # action succeeds, we have a target. One could picture being able to + # lock certain rooms from flight/dive, here we allow it as long as there + # is a suitable room above/below. + caller.location.msg_contents(f"$You() {action} {direction}.", from_obj=caller) + caller.move_to(target, quiet=True) + target.msg_contents( + f"$You() {action} from {from_direction}.", from_obj=caller, exclude=[caller] + ) + + +class XYZGridFlyDiveCmdSet(CmdSet): + """ + Optional cmdset if you want the fly/dive commands to move in a 3D environment. + + """ + + key = "xyzgrid_flydive_cmdset" + + def at_cmdset_creation(self): + self.add(CmdFlyAndDive()) diff --git a/evennia/contrib/grid/xyzgrid/tests.py b/evennia/contrib/grid/xyzgrid/tests.py index 11702109fc..4e8b229bdb 100644 --- a/evennia/contrib/grid/xyzgrid/tests.py +++ b/evennia/contrib/grid/xyzgrid/tests.py @@ -7,11 +7,10 @@ from random import randint from unittest import mock from django.test import TestCase +from evennia.utils.test_resources import BaseEvenniaCommandTest, BaseEvenniaTest from parameterized import parameterized -from evennia.utils.test_resources import BaseEvenniaTest - -from . import xymap, xymap_legend, xyzgrid, xyzroom +from . import commands, xymap, xymap_legend, xyzgrid, xyzroom MAP1 = """ @@ -341,6 +340,54 @@ MAP12b = r""" """ +MAP13a = r""" + ++ 0 1 + +1 #-# + | +0 # + ++ 0 1 + +""" + +MAP13b = r""" + ++ 0 1 + +1 # + +0 + ++ 0 1 + +""" + +MAP13c = r""" + ++ 0 1 + +1 # + +0 + ++ 0 1 + +""" + +MAP13d = r""" + ++ 0 1 + +1 #-# + | +0 # + ++ 0 1 + +""" + class _MapTest(BaseEvenniaTest): """ @@ -517,8 +564,10 @@ class TestMap2(_MapTest): ((1, 0), "#-#-#-#\n| | \n#-#-#--\n | \n @-#-#"), ( (2, 2), - " #---#\n | |\n# | #\n| | \n#-#-@-#--\n| " - "| \n#-#-#---#\n | |\n #-#-#-#", + ( + " #---#\n | |\n# | #\n| | \n#-#-@-#--\n| " + "| \n#-#-#---#\n | |\n #-#-#-#" + ), ), ((4, 5), "#-#-@ \n| | \n#---# \n| | \n| #-#"), ((5, 2), "--# \n | \n #-#\n |\n#---@\n \n--#-#\n | \n#-# "), @@ -577,8 +626,10 @@ class TestMap2(_MapTest): (2, 2), 2, None, - " # \n | \n #---# \n | \n | \n | \n" - "#-#-@-#---#\n | \n #-#---# ", + ( + " # \n | \n #---# \n | \n | \n | \n" + "#-#-@-#---#\n | \n #-#---# " + ), ), ((2, 2), 2, (5, 5), " | \n | \n#-@-#\n | \n#-#--"), # limit display size ((2, 2), 4, (3, 3), " | \n-@-\n | "), @@ -648,8 +699,10 @@ class TestMap3(_MapTest): (2, 2), 2, None, - " # \n / \n # / \n |/ \n # #\n |\\ / \n # @-# \n " - "|/ \\ \n # #\n / \\ \n# # ", + ( + " # \n / \n # / \n |/ \n # #\n |\\ / \n # @-# \n" + " |/ \\ \n # #\n / \\ \n# # " + ), ), ((5, 2), 2, None, " # \n | \n # \n / \\ \n# @\n \\ / \n # \n | \n # "), ] @@ -879,8 +932,7 @@ class TestMap8(_MapTest): (2, 2), 1, None, - " #-o \n | \n# o \n| | \no-o-@-#\n " - "| \n o \n | \n # ", + " #-o \n | \n# o \n| | \no-o-@-#\n | \n o \n | \n # ", ), ] ) @@ -901,24 +953,24 @@ class TestMap8(_MapTest): (3, 2), 1, None, - " #-o \n | \n# o \n| | \no-o-@..\n | \n o " - "\n | \n # ", + " #-o \n | \n# o \n| | \no-o-@..\n | \n o \n | \n # ", ), ( (2, 2), (5, 3), 1, None, - " #-o \n | \n# o \n| | \no-o-@-#\n . \n . " - "\n . \n ...", + " #-o \n | \n# o \n| | \no-o-@-#\n . \n . \n . \n ...", ), ( (2, 2), (5, 3), 2, None, - "#-#-o \n| \\| \n#-o-o-# .\n| |\\ .\no-o-@-" - "# .\n . . \n . . \n . . \n#---... ", + ( + "#-#-o \n| \\| \n#-o-o-# .\n| |\\ .\no-o-@-" + "# .\n . . \n . . \n . . \n#---... " + ), ), ((5, 3), (2, 2), 2, (13, 7), " o-o\n | |\n o-@\n .\n. .\n. . "), ( @@ -926,8 +978,10 @@ class TestMap8(_MapTest): (1, 1), 2, None, - " o-o\n | |\n o-@\n. .\n..... " - ".\n . . \n . . \n . . \n#---... ", + ( + " o-o\n | |\n o-@\n. .\n..... " + ".\n . . \n . . \n . . \n#---... " + ), ), ] ) @@ -1501,3 +1555,49 @@ class TestCallbacks(BaseEvenniaTest): self.assertEqual( mock_exit_callbacks.at_object_creation.mock_calls, [mock.call(), mock.call()] ) + + +class TestFlyDiveCommand(BaseEvenniaCommandTest): + def setUp(self): + super().setUp() + + self.grid, err = xyzgrid.XYZGrid.create("testgrid") + + self.map_data13a = {"map": MAP13a, "zcoord": -2} + self.map_data13b = {"map": MAP13b, "zcoord": -1} + self.map_data13c = {"map": MAP13c, "zcoord": 0} + self.map_data13d = {"map": MAP13d, "zcoord": 1} # not contiguous + + self.grid.add_maps(self.map_data13a, self.map_data13b, self.map_data13c, self.map_data13d) + self.grid.spawn() + + def tearDown(self): + self.grid.delete() + + @parameterized.expand( + [ + # startcoord, cmd, succeed?, endcoord + ((0, 0, -2), "fly", False, (0, 0, -2)), + ((1, 1, -2), "fly", True, (1, 1, -1)), + ((1, 1, -1), "fly", True, (1, 1, 0)), + ((1, 1, 0), "fly", True, (1, 1, 1)), + ((1, 1, 1), "fly", False, (1, 1, 1)), + ((0, 0, 1), "fly", False, (0, 0, 1)), + ((0, 0, 1), "dive", False, (0, 0, 1)), + ((1, 1, 1), "dive", True, (1, 1, 0)), + ((1, 1, 0), "dive", True, (1, 1, -1)), + ((1, 1, -1), "dive", True, (1, 1, -2)), + ((1, 1, -2), "dive", False, (1, 1, -2)), + ] + ) + def test_fly_and_dive(self, startcoord, cmdstring, success, endcoord): + """ + Test flying up and down and seeing if it works at different locations. + + """ + start_room = xyzgrid.XYZRoom.objects.get_xyz(xyz=startcoord) + self.char1.move_to(start_room) + + self.call(commands.CmdFlyAndDive(), "", "You" if success else "Can't", cmdstring=cmdstring) + + self.assertEqual(self.char1.location.xyz, endcoord) diff --git a/evennia/contrib/grid/xyzgrid/xyzgrid.py b/evennia/contrib/grid/xyzgrid/xyzgrid.py index a5143b8b4e..71935c4009 100644 --- a/evennia/contrib/grid/xyzgrid/xyzgrid.py +++ b/evennia/contrib/grid/xyzgrid/xyzgrid.py @@ -201,7 +201,7 @@ class XYZGrid(DefaultScript): """ for mapdata in mapdatas: zcoord = mapdata.get("zcoord") - if not zcoord: + if not zcoord is not None: raise RuntimeError("XYZGrid.add_map data must contain 'zcoord'.") self.db.map_data[zcoord] = mapdata diff --git a/evennia/contrib/grid/xyzgrid/xyzroom.py b/evennia/contrib/grid/xyzgrid/xyzroom.py index 94d890cdc4..5a9efcdd14 100644 --- a/evennia/contrib/grid/xyzgrid/xyzroom.py +++ b/evennia/contrib/grid/xyzgrid/xyzroom.py @@ -9,7 +9,6 @@ used as stand-alone XYZ-coordinate-aware rooms. from django.conf import settings from django.db.models import Q - from evennia.objects.manager import ObjectManager from evennia.objects.objects import DefaultExit, DefaultRoom @@ -290,11 +289,13 @@ class XYZRoom(DefaultRoom): if x is None or y is None or z is None: # don't cache unfinished coordinate (probably tags have not finished saving) return tuple( - int(coord) if coord is not None and coord.isdigit() else coord + int(coord) if coord is not None and coord.lstrip("-").isdigit() else coord for coord in (x, y, z) ) # cache result, convert to correct types (tags are strings) - self._xyz = tuple(int(coord) if coord.isdigit() else coord for coord in (x, y, z)) + self._xyz = tuple( + int(coord) if coord.lstrip("-").isdigit() else coord for coord in (x, y, z) + ) return self._xyz diff --git a/evennia/server/serversession.py b/evennia/server/serversession.py index 00cacc65eb..917f591e18 100644 --- a/evennia/server/serversession.py +++ b/evennia/server/serversession.py @@ -212,7 +212,7 @@ class ServerSession(_BASE_SESSION_CLASS): """ flags = self.protocol_flags - print("session flags:", flags) + # print("session flags:", flags) width = flags.get("SCREENWIDTH", {}).get(0, settings.CLIENT_DEFAULT_WIDTH) height = flags.get("SCREENHEIGHT", {}).get(0, settings.CLIENT_DEFAULT_HEIGHT) return width, height