From a69f4161629821b1ca04032cb29dcfdd7588a119 Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Mon, 21 Nov 2022 17:34:40 -0700 Subject: [PATCH 1/4] update wilderness contrib --- evennia/contrib/grid/wilderness/wilderness.py | 180 ++++++++---------- 1 file changed, 83 insertions(+), 97 deletions(-) diff --git a/evennia/contrib/grid/wilderness/wilderness.py b/evennia/contrib/grid/wilderness/wilderness.py index e2270057d1..7cb6ae854b 100644 --- a/evennia/contrib/grid/wilderness/wilderness.py +++ b/evennia/contrib/grid/wilderness/wilderness.py @@ -34,10 +34,11 @@ The defaults, while useable, are meant to be customised. When creating a new wilderness map it is possible to give a "map provider": this is a python object that is smart enough to create the map. -The default provider, `WildernessMapProvider`, just creates a grid area that +The default provider, `WildernessMapProvider`, creates a grid area that is unlimited in size. -This `WildernessMapProvider` can be subclassed to create more interesting -maps and also to customize the room/exit typeclass used. + +`WildernessMapProvider` can be subclassed to create more interesting maps +and also to customize the room/exit typeclass used. There is also no command that allows players to enter the wilderness. This still needs to be added: it can be a command or an exit, depending on your @@ -121,7 +122,7 @@ from evennia import ( create_script, ) from evennia.utils import inherits_from - +from evennia.typeclasses.attributes import AttributeProperty def create_wilderness(name="default", mapprovider=None): """ @@ -161,10 +162,12 @@ def enter_wilderness(obj, coordinates=(0, 0), name="default"): Returns: bool: True if obj successfully moved into the wilderness. """ - if not WildernessScript.objects.filter(db_key=name).exists(): + script = WildernessScript.objects.filter(db_key=name) + if not script.exists(): return False + else: + script = script[0] - script = WildernessScript.objects.get(db_key=name) if script.is_valid_coordinates(coordinates): script.move_obj(obj, coordinates) return True @@ -205,6 +208,18 @@ class WildernessScript(DefaultScript): into storage when they are not needed anymore. """ + # Stores the MapProvider class + mapprovider = AttributeProperty() + + # Stores a dictionary of items on the map with their coordinates + # The key is the item, the value are the coordinates as (x, y) tuple. + itemcoordinates = AttributeProperty() + + # Determines whether or not rooms are recycled despite containing non-player objects + # True means that leaving behind a non-player object will prevent the room from being recycled + # in order to preserve the object + preserve_items = AttributeProperty(default=False) + def at_script_creation(self): """ Only called once, when the script is created. This is a default Evennia @@ -224,39 +239,16 @@ class WildernessScript(DefaultScript): # allows quick retrieval if a new room is needed without having to # create it. self.db.unused_rooms = [] - - @property - def mapprovider(self): + + def at_server_start(self): """ - Shortcut property to the map provider. - - Returns: - MapProvider: the mapprovider used with this wilderness - """ - return self.db.mapprovider - - @property - def itemcoordinates(self): - """ - Returns a dictionary with the coordinates of every item inside this - wilderness map. The key is the item, the value are the coordinates as - (x, y) tuple. - - Returns: - {item: coordinates} - """ - return self.db.itemcoordinates - - def at_start(self): - """ - Called when the script is started and also after server reloads. + Called after the server is started or reloaded. """ for coordinates, room in self.db.rooms.items(): room.ndb.wildernessscript = self room.ndb.active_coordinates = coordinates - for item in list(self.db.itemcoordinates.keys()): - # Items deleted from the wilderness leave None type 'ghosts' - # that must be cleaned out + for item in self.db.itemcoordinates.keys(): + # Items deleted from the wilderness can leave None type 'ghosts' if item is None: del self.db.itemcoordinates[item] continue @@ -303,15 +295,7 @@ class WildernessScript(DefaultScript): Returns: [Object, ]: list of Objects at coordinates """ - result = [] - for item, item_coordinates in list(self.itemcoordinates.items()): - # Items deleted from the wilderness leave None type 'ghosts' - # that must be cleaned out - if item is None: - del self.db.itemcoordinates[item] - continue - if coordinates == item_coordinates: - result.append(item) + result = [ item for item, item_coords in self.itemcoordinates.items() if item_coords == coordinates and item is not None ] return result def move_obj(self, obj, new_coordinates): @@ -330,44 +314,37 @@ class WildernessScript(DefaultScript): # appear in its old room should that room be deleted. obj.location = None - try: - # See if we already have a room for that location - room = self.db.rooms[new_coordinates] + # By default, we'll assume we won't be making a new room and change this flag if necessary. + create_room = False + + # See if we already have a room for that location + if room := self.db.rooms.get(new_coordinates): # There is. Try to destroy the old_room if it is not needed anymore self._destroy_room(old_room) - except KeyError: + else: # There is no room yet at new_location - if (old_room and not inherits_from(old_room, WildernessRoom)) or (not old_room): - # Obj doesn't originally come from a wilderness room. - # We'll create a new one then. - room = self._create_room(new_coordinates, obj) - else: - # Obj does come from another wilderness room - create_new_room = False - - if old_room.wilderness != self: - # ... but that other wilderness room belongs to another - # wilderness map - create_new_room = True - old_room.wilderness.at_post_object_leave(obj) - else: - for item in old_room.contents: - if item.has_account: - # There is still a player in the old room. - # Let's create a new room and not touch that old - # room. - create_new_room = True - break - - if create_new_room: - # Create a new room to hold obj, not touching any obj's in - # the old room + # Is the old room in this wilderness? + if old_room in self.db.rooms.keys(): + # Is there anything still left in the old_room, besides the exits? + if len([ob for ob in old_room.contents if not inherits_from(ob, WildernessExit)]): + # There is, so we'll create a new room room = self._create_room(new_coordinates, obj) else: - # The old_room is empty: we are just going to reuse that - # room instead of creating a new one + # The room is empty, so we'll just reuse it room = old_room + # Is the previous room from a different wilderness? + elif inherits_from(old_room, WildernessRoom) and old_room.wilderness != self: + # It does, so we make sure to leave the other wilderness properly + old_room.wilderness.at_post_object_leave(obj) + # We'll also need to create a new room in this wilderness + room = self._create_room(new_coordinates, obj) + + else: + # Obj comes from outside the wilderness entirely + # We need to make a new room + room = self._create_room(new_coordinates, obj) + room.set_active_coordinates(new_coordinates, obj) obj.location = room obj.ndb.wilderness = self @@ -425,7 +402,11 @@ class WildernessScript(DefaultScript): def _destroy_room(self, room): """ Moves a room back to storage. If room is not a WildernessRoom or there - is a player inside the room, then this does nothing. + is something left inside the room, then this does nothing. + + Implementation note: If `preserve_items` is False (the default) then any + objects left in the rooms will be moved to None. You may want to implement + your own cleanup or recycling routine for these objects. Args: room (WildernessRoom): the room to put in storage @@ -433,25 +414,30 @@ class WildernessScript(DefaultScript): if not room or not inherits_from(room, WildernessRoom): return + # Check the contents of the room before recycling for item in room.contents: if item.has_account: - # There is still a character in that room. We can't get rid of - # it just yet - break - else: - # No characters left in the room. + # There is still a player in this room, we can't delete it yet. + return - # Clear the location of every obj in that room first - for item in room.contents: - if item.destination and item.destination == room: - # Ignore the exits, they stay in the room - continue - item.location = None + if not (item.destination and item.destination == room): + # There is still a non-exit object in the room. Should we preserve it? + if self.preserve_items: + # Yes, so we can't get rid of the room just yet + return - # Then delete its reference - del self.db.rooms[room.ndb.active_coordinates] - # And finally put this room away in storage - self.db.unused_rooms.append(room) + # If we get here, the room can be recycled + # Clear the location of any objects left in that room first + for item in room.contents: + if item.destination and item.destination == room: + # Ignore the exits, they stay in the room + continue + item.location = None + + # Then delete its coordinate reference + del self.db.rooms[room.ndb.active_coordinates] + # And finally put this room away in storage + self.db.unused_rooms.append(room) def at_post_object_leave(self, obj): """ @@ -460,13 +446,13 @@ class WildernessScript(DefaultScript): Args: obj (object): the object that left """ - # Remove that obj from the wilderness's coordinates dict - loc = self.db.itemcoordinates[obj] - del self.db.itemcoordinates[obj] - - # And see if we can put that room away into storage. - room = self.db.rooms[loc] - self._destroy_room(room) + # Try removing the object from the coordinates system + if loc := self.db.itemcoordinates.pop(obj, None): + # The object was removed successfully + # Make sure there was a room at that location + if room := self.db.rooms.get(loc): + # If so, try to clean up the room + self._destroy_room(room) class WildernessRoom(DefaultRoom): From ab317a691e99b979e259e4dfeadabec440f7a340 Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Mon, 21 Nov 2022 18:17:03 -0700 Subject: [PATCH 2/4] some more updates and cleanup --- evennia/contrib/grid/wilderness/tests.py | 6 +- evennia/contrib/grid/wilderness/wilderness.py | 62 +++++++++++-------- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/evennia/contrib/grid/wilderness/tests.py b/evennia/contrib/grid/wilderness/tests.py index fee2fd17e4..af54bebd26 100644 --- a/evennia/contrib/grid/wilderness/tests.py +++ b/evennia/contrib/grid/wilderness/tests.py @@ -36,14 +36,14 @@ class TestWilderness(BaseEvenniaTest): wilderness.enter_wilderness(self.char1) self.assertIsInstance(self.char1.location, wilderness.WildernessRoom) w = self.get_wilderness_script() - self.assertEqual(w.db.itemcoordinates[self.char1], (0, 0)) + self.assertEqual(w.itemcoordinates[self.char1], (0, 0)) def test_enter_wilderness_custom_coordinates(self): wilderness.create_wilderness() wilderness.enter_wilderness(self.char1, coordinates=(1, 2)) self.assertIsInstance(self.char1.location, wilderness.WildernessRoom) w = self.get_wilderness_script() - self.assertEqual(w.db.itemcoordinates[self.char1], (1, 2)) + self.assertEqual(w.itemcoordinates[self.char1], (1, 2)) def test_enter_wilderness_custom_name(self): name = "customnname" @@ -133,6 +133,6 @@ class TestWilderness(BaseEvenniaTest): "west": (0, 1), "northwest": (0, 2), } - for (direction, correct_loc) in directions.items(): # Not compatible with Python 3 + for direction, correct_loc in directions.items(): new_loc = wilderness.get_new_coordinates(loc, direction) self.assertEqual(new_loc, correct_loc, direction) diff --git a/evennia/contrib/grid/wilderness/wilderness.py b/evennia/contrib/grid/wilderness/wilderness.py index 7cb6ae854b..9aef3958b5 100644 --- a/evennia/contrib/grid/wilderness/wilderness.py +++ b/evennia/contrib/grid/wilderness/wilderness.py @@ -92,7 +92,7 @@ class PyramidMapProvider(wilderness.WildernessMapProvider): desc = "This is a room in the pyramid." if y == 3 : desc = "You can see far and wide from the top of the pyramid." - room.db.desc = desc + room.ndb.desc = desc ``` Now we can use our new pyramid-shaped wilderness map. From inside Evennia we @@ -103,14 +103,16 @@ create a new wilderness (with the name "default") but using our new map provider ## Implementation details - When a character moves into the wilderness, they get their own room. If - they move, instead of moving the character, the room changes to match the - new coordinates. - If a character meets another character in the wilderness, then their room - merges. When one of the character leaves again, they each get their own - separate rooms. - Rooms are created as needed. Unneeded rooms are stored away to avoid the - overhead cost of creating new rooms again in the future. +When a character moves into the wilderness, they get their own room. If +they move, instead of moving the character, the room changes to match the +new coordinates. + +If a character meets another character in the wilderness, then their room +merges. When one of the character leaves again, they each get their own +separate rooms. + +Rooms are created as needed. Unneeded rooms are stored away to avoid the +overhead cost of creating new rooms again in the future. """ @@ -247,8 +249,10 @@ class WildernessScript(DefaultScript): for coordinates, room in self.db.rooms.items(): room.ndb.wildernessscript = self room.ndb.active_coordinates = coordinates + self.wilderness.mapprovider.at_prepare_room(coordinates, None, self) for item in self.db.itemcoordinates.keys(): - # Items deleted from the wilderness can leave None type 'ghosts' + # Items deleted while in the wilderness can leave None-type 'ghosts' + # These need to be cleaned up if item is None: del self.db.itemcoordinates[item] continue @@ -296,7 +300,7 @@ class WildernessScript(DefaultScript): [Object, ]: list of Objects at coordinates """ result = [ item for item, item_coords in self.itemcoordinates.items() if item_coords == coordinates and item is not None ] - return result + return list(result) def move_obj(self, obj, new_coordinates): """ @@ -508,21 +512,13 @@ class WildernessRoom(DefaultRoom): # n, ne, ... exits. return - itemcoords = self.wilderness.db.itemcoordinates + itemcoords = self.wilderness.itemcoordinates if moved_obj in itemcoords: # This object was already in the wilderness. We need to make sure # it goes to the correct room it belongs to. - # Otherwise the following issue can come up: - # 1) Player 1 and Player 2 share a room - # 2) Player 1 disconnects - # 3) Player 2 moves around - # 4) Player 1 reconnects - # Player 1 will end up in player 2's room, which has the wrong - # coordinates - coordinates = itemcoords[moved_obj] # Setting the location to None is important here so that we always - # get a "fresh" room + # get a "fresh" room if it was in the wrong place moved_obj.location = None self.wilderness.move_obj(moved_obj, coordinates) else: @@ -550,14 +546,15 @@ class WildernessRoom(DefaultRoom): obj (Object): the object that moved into this room and caused the coordinates to change """ - # Remove the reference for the old coordinates... + # Remove any reference for the old coordinates... rooms = self.wilderness.db.rooms - del rooms[self.coordinates] + if self.coordinates: + del rooms[self.coordinates] # ...and add it for the new coordinates. self.ndb.active_coordinates = new_coordinates rooms[self.coordinates] = self - # Every obj inside this room will get its location set to None + # Any object inside this room will get its location set to None for item in self.contents: if not item.destination or item.destination != item.location: item.location = None @@ -586,6 +583,7 @@ class WildernessRoom(DefaultRoom): def get_display_name(self, looker, **kwargs): """ Displays the name of the object in a viewer-aware manner. + This is a core evennia hook. Args: looker (TypedObject): The object or account that is looking @@ -610,7 +608,21 @@ class WildernessRoom(DefaultRoom): name += " {0}".format(self.coordinates) return name + + def get_display_desc(self, looker, **kwargs): + """ + Displays the description of the room. This is a core evennia hook. + + Allows the room's description to be customized in an ndb value, + avoiding having to write to the database on moving. + """ + # Check if a new description was prepared by the map provider + if self.ndb.active_desc: + # There is one: use it + return self.ndb.active_desc + # Otherwise, use the normal description hook. + return super().get_display_desc(looker, **kwargs) class WildernessExit(DefaultExit): """ @@ -754,7 +766,7 @@ class WildernessMapProvider(object): Args: coordinates (tuple): the coordinates as (x, y) where room is located at - caller (Object): the object that moved into this room + caller (Object or None): the object that moved into this room room (WildernessRoom): the room object that will be used at that wilderness location Example: From de24d2646d7b2bf31384fd2738d0717903353a3a Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Mon, 21 Nov 2022 20:23:56 -0700 Subject: [PATCH 3/4] add test for preserve_items opt and fix --- evennia/contrib/grid/wilderness/tests.py | 27 ++++++ evennia/contrib/grid/wilderness/wilderness.py | 85 ++++++++++++------- 2 files changed, 81 insertions(+), 31 deletions(-) diff --git a/evennia/contrib/grid/wilderness/tests.py b/evennia/contrib/grid/wilderness/tests.py index af54bebd26..ec7304e100 100644 --- a/evennia/contrib/grid/wilderness/tests.py +++ b/evennia/contrib/grid/wilderness/tests.py @@ -136,3 +136,30 @@ class TestWilderness(BaseEvenniaTest): for direction, correct_loc in directions.items(): new_loc = wilderness.get_new_coordinates(loc, direction) self.assertEqual(new_loc, correct_loc, direction) + + def test_preserve_items(self): + wilderness.create_wilderness() + w = self.get_wilderness_script() + + # move char and obj to wilderness + wilderness.enter_wilderness(self.char1) + wilderness.enter_wilderness(self.obj1) + + # move to a new room + w.move_obj(self.char1, (1, 1)) + # the room should be remapped and 0,0 should not exist + self.assertTrue((0, 0) not in w.db.rooms) + self.assertEqual(1, len(w.db.rooms)) + # verify obj1 moved to None + self.assertIsNone(self.obj1.location) + + # now change to preserve items + w.preserve_items = True + wilderness.enter_wilderness(self.obj1, (1, 1)) + # move the character again + w.move_obj(self.char1, (0, 1)) + # check that the previous room was preserved + self.assertIn((1, 1), w.db.rooms) + self.assertEqual(2, len(w.db.rooms)) + # and verify that obj1 is still at 1,1 + self.assertEqual(self.obj1.location, w.db.rooms[(1, 1)]) diff --git a/evennia/contrib/grid/wilderness/wilderness.py b/evennia/contrib/grid/wilderness/wilderness.py index 9aef3958b5..977be44454 100644 --- a/evennia/contrib/grid/wilderness/wilderness.py +++ b/evennia/contrib/grid/wilderness/wilderness.py @@ -46,11 +46,11 @@ needs. ## Example - To give an example of how to customize, we will create a very simple (and - small) wilderness map that is shaped like a pyramid. The map will be - provided as a string: a "." symbol is a location we can walk on. +To give an example of how to customize, we will create a very simple (and +small) wilderness map that is shaped like a pyramid. The map will be +provided as a string: a "." symbol is a location we can walk on. - Let's create a file world/pyramid.py: +Let's create a file world/pyramid.py: ```python map_str = ''' @@ -113,7 +113,6 @@ separate rooms. Rooms are created as needed. Unneeded rooms are stored away to avoid the overhead cost of creating new rooms again in the future. - """ from evennia import ( @@ -126,6 +125,7 @@ from evennia import ( from evennia.utils import inherits_from from evennia.typeclasses.attributes import AttributeProperty + def create_wilderness(name="default", mapprovider=None): """ Creates a new wilderness map. Does nothing if a wilderness map already @@ -216,7 +216,7 @@ class WildernessScript(DefaultScript): # Stores a dictionary of items on the map with their coordinates # The key is the item, the value are the coordinates as (x, y) tuple. itemcoordinates = AttributeProperty() - + # Determines whether or not rooms are recycled despite containing non-player objects # True means that leaving behind a non-player object will prevent the room from being recycled # in order to preserve the object @@ -241,7 +241,7 @@ class WildernessScript(DefaultScript): # allows quick retrieval if a new room is needed without having to # create it. self.db.unused_rooms = [] - + def at_server_start(self): """ Called after the server is started or reloaded. @@ -249,7 +249,6 @@ class WildernessScript(DefaultScript): for coordinates, room in self.db.rooms.items(): room.ndb.wildernessscript = self room.ndb.active_coordinates = coordinates - self.wilderness.mapprovider.at_prepare_room(coordinates, None, self) for item in self.db.itemcoordinates.keys(): # Items deleted while in the wilderness can leave None-type 'ghosts' # These need to be cleaned up @@ -299,7 +298,11 @@ class WildernessScript(DefaultScript): Returns: [Object, ]: list of Objects at coordinates """ - result = [ item for item, item_coords in self.itemcoordinates.items() if item_coords == coordinates and item is not None ] + result = [ + item + for item, item_coords in self.itemcoordinates.items() + if item_coords == coordinates and item is not None + ] return list(result) def move_obj(self, obj, new_coordinates): @@ -318,38 +321,56 @@ class WildernessScript(DefaultScript): # appear in its old room should that room be deleted. obj.location = None - # By default, we'll assume we won't be making a new room and change this flag if necessary. - create_room = False - # See if we already have a room for that location if room := self.db.rooms.get(new_coordinates): # There is. Try to destroy the old_room if it is not needed anymore self._destroy_room(old_room) else: # There is no room yet at new_location - # Is the old room in this wilderness? - if old_room in self.db.rooms.keys(): - # Is there anything still left in the old_room, besides the exits? - if len([ob for ob in old_room.contents if not inherits_from(ob, WildernessExit)]): - # There is, so we'll create a new room - room = self._create_room(new_coordinates, obj) - else: - # The room is empty, so we'll just reuse it - room = old_room + # Is the old room in a wilderness? + if hasattr(old_room, "wilderness"): + # Yes. Is it in THIS wilderness? + if old_room.wilderness == self: + # Should we preserve rooms with any objects? + if self.preserve_items: + # Yes - check if ANY objects besides the exits are in old_room + if len( + [ + ob + for ob in old_room.contents + if not inherits_from(ob, WildernessExit) + ] + ): + # There is, so we'll create a new room + room = self._create_room(new_coordinates, obj) + else: + # The room is empty, so we'll reuse it + room = old_room + else: + # Only preserve rooms if there are players behind + if len([ob for ob in old_room.contents if ob.has_account]): + # There is still a player there; create a new room + room = self._create_room(new_coordinates, obj) + else: + # The room is empty of players, so we'll reuse it + room = old_room - # Is the previous room from a different wilderness? - elif inherits_from(old_room, WildernessRoom) and old_room.wilderness != self: - # It does, so we make sure to leave the other wilderness properly - old_room.wilderness.at_post_object_leave(obj) - # We'll also need to create a new room in this wilderness - room = self._create_room(new_coordinates, obj) + # It's in a different wilderness + else: + # It does, so we make sure to leave the other wilderness properly + old_room.wilderness.at_post_object_leave(obj) + # We'll also need to create a new room in this wilderness + room = self._create_room(new_coordinates, obj) else: # Obj comes from outside the wilderness entirely # We need to make a new room room = self._create_room(new_coordinates, obj) - room.set_active_coordinates(new_coordinates, obj) + # Set `room` to the new coordinates, however it was made + room.set_active_coordinates(new_coordinates, obj) + + # Put obj back, now in the correct room obj.location = room obj.ndb.wilderness = self @@ -407,7 +428,7 @@ class WildernessScript(DefaultScript): """ Moves a room back to storage. If room is not a WildernessRoom or there is something left inside the room, then this does nothing. - + Implementation note: If `preserve_items` is False (the default) then any objects left in the rooms will be moved to None. You may want to implement your own cleanup or recycling routine for these objects. @@ -555,6 +576,7 @@ class WildernessRoom(DefaultRoom): rooms[self.coordinates] = self # Any object inside this room will get its location set to None + # unless it's a wilderness exit for item in self.contents: if not item.destination or item.destination != item.location: item.location = None @@ -608,11 +630,11 @@ class WildernessRoom(DefaultRoom): name += " {0}".format(self.coordinates) return name - + def get_display_desc(self, looker, **kwargs): """ Displays the description of the room. This is a core evennia hook. - + Allows the room's description to be customized in an ndb value, avoiding having to write to the database on moving. """ @@ -624,6 +646,7 @@ class WildernessRoom(DefaultRoom): # Otherwise, use the normal description hook. return super().get_display_desc(looker, **kwargs) + class WildernessExit(DefaultExit): """ This is an Exit object used inside a WildernessRoom. Instead of changing From eda361d7d6c8218a48f7679008df72dffbe08c90 Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Mon, 21 Nov 2022 20:40:10 -0700 Subject: [PATCH 4/4] minor docs change, add `preserve_items` to create method --- evennia/contrib/grid/wilderness/README.md | 30 +++++++--- evennia/contrib/grid/wilderness/wilderness.py | 60 +++++++++---------- 2 files changed, 48 insertions(+), 42 deletions(-) diff --git a/evennia/contrib/grid/wilderness/README.md b/evennia/contrib/grid/wilderness/README.md index 1e83975e4b..261060eceb 100644 --- a/evennia/contrib/grid/wilderness/README.md +++ b/evennia/contrib/grid/wilderness/README.md @@ -5,7 +5,7 @@ Contribution by titeuf87, 2017 This contrib provides a wilderness map without actually creating a large number of rooms - as you move, you instead end up back in the same room but its description changes. This means you can make huge areas with little database use as -long as the rooms are relatively similar (name/desc changing). +long as the rooms are relatively similar (e.g. only the names/descs changing). ## Installation @@ -29,6 +29,9 @@ All coordinates used by the wilderness map are in the format of `(x, y)` tuples. x goes from left to right and y goes from bottom to top. So `(0, 0)` is the bottom left corner of the map. +> You can also add a wilderness by defining a WildernessScript in your GLOBAL_SCRIPT +> settings. If you do, make sure define the map provider. + ## Customisation The defaults, while useable, are meant to be customised. When creating a @@ -37,9 +40,14 @@ python object that is smart enough to create the map. The default provider, `WildernessMapProvider`, just creates a grid area that is unlimited in size. -This `WildernessMapProvider` can be subclassed to create more interesting + +`WildernessMapProvider` can be subclassed to create more interesting maps and also to customize the room/exit typeclass used. +The `WildernessScript` also has an optional `preserve_items` property, which +when set to `True` will not recycle rooms that contain any objects. By default, +a wilderness room is recycled whenever there are no players left in it. + There is also no command that allows players to enter the wilderness. This still needs to be added: it can be a command or an exit, depending on your needs. @@ -94,7 +102,7 @@ class PyramidMapProvider(wilderness.WildernessMapProvider): desc = "This is a room in the pyramid." if y == 3 : desc = "You can see far and wide from the top of the pyramid." - room.db.desc = desc + room.ndb.desc = desc ``` Now we can use our new pyramid-shaped wilderness map. From inside Evennia we @@ -105,9 +113,13 @@ create a new wilderness (with the name "default") but using our new map provider ## Implementation details -When a character moves into the wilderness, they get their own room. If they -move, instead of moving the character, the room changes to match the new -coordinates. If a character meets another character in the wilderness, then -their room merges. When one of the character leaves again, they each get their -own separate rooms. Rooms are created as needed. Unneeded rooms are stored away -to avoid the overhead cost of creating new rooms again in the future. +When a character moves into the wilderness, they get their own room. If +they move, instead of moving the character, the room changes to match the +new coordinates. + +If a character meets another character in the wilderness, then their room +merges. When one of the character leaves again, they each get their own +separate rooms. + +Rooms are created as needed. Unneeded rooms are stored away to avoid the +overhead cost of creating new rooms again in the future. diff --git a/evennia/contrib/grid/wilderness/wilderness.py b/evennia/contrib/grid/wilderness/wilderness.py index 977be44454..07274bf120 100644 --- a/evennia/contrib/grid/wilderness/wilderness.py +++ b/evennia/contrib/grid/wilderness/wilderness.py @@ -46,11 +46,11 @@ needs. ## Example -To give an example of how to customize, we will create a very simple (and -small) wilderness map that is shaped like a pyramid. The map will be -provided as a string: a "." symbol is a location we can walk on. + To give an example of how to customize, we will create a very simple (and + small) wilderness map that is shaped like a pyramid. The map will be + provided as a string: a "." symbol is a location we can walk on. -Let's create a file world/pyramid.py: + Let's create a file world/pyramid.py: ```python map_str = ''' @@ -103,16 +103,17 @@ create a new wilderness (with the name "default") but using our new map provider ## Implementation details -When a character moves into the wilderness, they get their own room. If -they move, instead of moving the character, the room changes to match the -new coordinates. + When a character moves into the wilderness, they get their own room. If + they move, instead of moving the character, the room changes to match the + new coordinates. -If a character meets another character in the wilderness, then their room -merges. When one of the character leaves again, they each get their own -separate rooms. + If a character meets another character in the wilderness, then their room + merges. When one of the character leaves again, they each get their own + separate rooms. + + Rooms are created as needed. Unneeded rooms are stored away to avoid the + overhead cost of creating new rooms again in the future. -Rooms are created as needed. Unneeded rooms are stored away to avoid the -overhead cost of creating new rooms again in the future. """ from evennia import ( @@ -125,8 +126,7 @@ from evennia import ( from evennia.utils import inherits_from from evennia.typeclasses.attributes import AttributeProperty - -def create_wilderness(name="default", mapprovider=None): +def create_wilderness(name="default", mapprovider=None, preserve_items=False): """ Creates a new wilderness map. Does nothing if a wilderness map already exists with the same name. @@ -147,6 +147,8 @@ def create_wilderness(name="default", mapprovider=None): mapprovider = WildernessMapProvider() script = create_script(WildernessScript, key=name) script.db.mapprovider = mapprovider + if preserve_items: + script.preserve_items = True def enter_wilderness(obj, coordinates=(0, 0), name="default"): @@ -216,7 +218,7 @@ class WildernessScript(DefaultScript): # Stores a dictionary of items on the map with their coordinates # The key is the item, the value are the coordinates as (x, y) tuple. itemcoordinates = AttributeProperty() - + # Determines whether or not rooms are recycled despite containing non-player objects # True means that leaving behind a non-player object will prevent the room from being recycled # in order to preserve the object @@ -241,7 +243,7 @@ class WildernessScript(DefaultScript): # allows quick retrieval if a new room is needed without having to # create it. self.db.unused_rooms = [] - + def at_server_start(self): """ Called after the server is started or reloaded. @@ -298,11 +300,7 @@ class WildernessScript(DefaultScript): Returns: [Object, ]: list of Objects at coordinates """ - result = [ - item - for item, item_coords in self.itemcoordinates.items() - if item_coords == coordinates and item is not None - ] + result = [ item for item, item_coords in self.itemcoordinates.items() if item_coords == coordinates and item is not None ] return list(result) def move_obj(self, obj, new_coordinates): @@ -321,6 +319,9 @@ class WildernessScript(DefaultScript): # appear in its old room should that room be deleted. obj.location = None + # By default, we'll assume we won't be making a new room and change this flag if necessary. + create_room = False + # See if we already have a room for that location if room := self.db.rooms.get(new_coordinates): # There is. Try to destroy the old_room if it is not needed anymore @@ -334,13 +335,7 @@ class WildernessScript(DefaultScript): # Should we preserve rooms with any objects? if self.preserve_items: # Yes - check if ANY objects besides the exits are in old_room - if len( - [ - ob - for ob in old_room.contents - if not inherits_from(ob, WildernessExit) - ] - ): + if len([ob for ob in old_room.contents if not inherits_from(ob, WildernessExit)]): # There is, so we'll create a new room room = self._create_room(new_coordinates, obj) else: @@ -428,7 +423,7 @@ class WildernessScript(DefaultScript): """ Moves a room back to storage. If room is not a WildernessRoom or there is something left inside the room, then this does nothing. - + Implementation note: If `preserve_items` is False (the default) then any objects left in the rooms will be moved to None. You may want to implement your own cleanup or recycling routine for these objects. @@ -630,11 +625,11 @@ class WildernessRoom(DefaultRoom): name += " {0}".format(self.coordinates) return name - + def get_display_desc(self, looker, **kwargs): """ Displays the description of the room. This is a core evennia hook. - + Allows the room's description to be customized in an ndb value, avoiding having to write to the database on moving. """ @@ -646,7 +641,6 @@ class WildernessRoom(DefaultRoom): # Otherwise, use the normal description hook. return super().get_display_desc(looker, **kwargs) - class WildernessExit(DefaultExit): """ This is an Exit object used inside a WildernessRoom. Instead of changing @@ -789,7 +783,7 @@ class WildernessMapProvider(object): Args: coordinates (tuple): the coordinates as (x, y) where room is located at - caller (Object or None): the object that moved into this room + caller (Object): the object that moved into this room room (WildernessRoom): the room object that will be used at that wilderness location Example: