Add fly/dive command to XYZGrid to simulate full 3D movement

This commit is contained in:
Griatch 2023-01-27 22:23:41 +01:00
parent 0938bf45fd
commit 55d2a67cc6
7 changed files with 304 additions and 43 deletions

View file

@ -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

View file

@ -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

View file

@ -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())

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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