mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Add fly/dive command to XYZGrid to simulate full 3D movement
This commit is contained in:
parent
0938bf45fd
commit
55d2a67cc6
7 changed files with 304 additions and 43 deletions
10
CHANGELOG.md
10
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue