""" Room Rooms are simple containers that has no location of their own. """ from collections import defaultdict import random from evennia import logger, search_tag from evennia import DefaultRoom from evennia.utils.utils import list_to_string from evennia.utils import inherits_from from utils import spath from utils.utils import has_tag, fmt_light, fmt_dark, has_effect from typeclasses.characters import Character MAP_SIZE = 128 class Room(DefaultRoom): """ Rooms are like any Object, except their location is None (which is default). They also use basetype_setup() to add locks so they cannot be puppeted or picked up. (to change that, use at_object_creation instead) See examples/object.py for a list of properties and methods available on all Objects. """ def at_object_creation(self): self.locks.add("light:false()") self.db.x = -1 self.db.y = -1 self.db.map_icon = '|w⊡|n' self.db.zone = None class IndoorRoom(Room): DARK_MESSAGES = ( "It is pitch black. You are likely to be eaten by a grue.", "It's pitch black. You fumble around but cannot find anything.", "You don't see a thing. You feel around, managing to bump your fingers hard against something. Ouch!", "You don't see a thing! Blindly grasping the air around you, you find nothing.", "It's totally dark here. You almost stumble over some un-evenness in the ground.", "You are completely blind. For a moment you think you hear someone breathing nearby ... " "\n ... surely you must be mistaken.", "Blind, you think you find some sort of object on the ground, but it turns out to be just a stone.", "Blind, you bump into a wall. The wall seems to be covered with some sort of vegetation," " but its too damp to burn.", "You can't see anything, but the air is damp. It feels like you are far underground.", ) map_icon = '|w□|n' def at_object_creation(self): super().at_object_creation() self.locks.add("search:all()") self.db.is_lit = False self.db.dark_desc = "" def at_init(self): """ Called when room is first recached (such as after a reload) """ super().at_init() self.check_light_state() def return_appearance(self, looker, **kwargs): """ This formats a description. It is the hook a 'look' command should call. Args: looker (Object): Object doing the looking. **kwargs (dict): Arbitrary, optional arguments for users overriding the call (unused by default). """ if not looker: return "" # get and identify all objects visible = (con for con in self.contents if con != looker and con.access(looker, "view")) features, exits, users, things = [], [], [], defaultdict(list) for con in visible: key = con.get_display_name(looker) if con.destination: if con.ndb.directional_alias: exits.append("|m{}|n|w)|n{}".format(con.ndb.directional_alias, key)) else: exits.append(key) elif con.has_account: users.append("|c%s|n" % key) elif con.db.feature_desc: features.append(con.db.feature_desc) else: # things can be pluralized things[key].append(con) # get description, build string string = "{}\n".format(self.get_display_name(looker)) string += "-" * 100 string += "|/" if not self.db.is_lit and not looker.is_superuser: desc = self.db.dark_desc if self.db.dark_desc else random.choice(self.DARK_MESSAGES) else: desc = self.db.desc if desc: string += "{}".format(desc) if features and (self.db.is_lit or looker.is_superuser): for feature in features: string += "|/{}".format(feature) if things and (self.db.is_lit or looker.is_superuser): # handle pluralization of things (never pluralize users) thing_strings = [] for key, itemlist in sorted(things.items()): nitem = len(itemlist) if nitem == 1: key, _ = itemlist[0].get_numbered_name( nitem, looker, key=key) else: key = [item.get_numbered_name(nitem, looker, key=key)[ 1] for item in itemlist][0] thing_strings.append(key) string += "|/In this place you see " if len(thing_strings) == 1: string += "|w{}|n.".format(thing_strings[0]) else: for idx, thing in enumerate(thing_strings): if idx != len(thing_strings) - 1: string += "|w{}|n, ".format(thing) else: string += "and |w{}|n.".format(thing) if exits: string += "|/" string += "-" * 100 string += "\n|wExits:|n " + list_to_string(exits) if users: if self.db.is_lit or looker.is_superuser: string += "\n|wYou see:|n " + list_to_string(users) else: string += "\n|wYou sense you are not alone...|n" return string def check_light_state(self, exclude=None): changed = False # there is an object emitting light? if any(self._carries_light(obj) for obj in self.contents if obj != exclude): if not self.db.is_lit: changed = True self.msg_contents(fmt_light("The room lights up.")) self.db.is_lit = True # show objects in room but not chars or exits # for obj in self.contents: # if not obj.has_account and not obj.destination: # obj.locks.add('view:all()') else: if self.db.is_lit: changed = True self.msg_contents(fmt_dark("Darkness falls."), exclude) # no one is carrying light - darken the room self.db.is_lit = False # hidden objects in room but not chars or exits # for obj in self.contents: # if not obj.has_account and not obj.destination: # obj.locks.add('view:false()') return changed def _carries_light(self, obj): """ Checks if the given object carries anything that gives light. Note that we do NOT look for a specific LightSource typeclass, but for the Attribute is_giving_light - this makes it easy to later add other types of light-giving items. We also accept if there is a light-giving object in the room overall (like if a splinter was dropped in the room) """ return ( has_effect(obj, 'emit_light') or has_effect(obj, 'emit_magic_light') or any(o for o in obj.contents if (has_effect(o, 'emit_light') or has_effect(o, 'emit_magic_light')) and not inherits_from(obj, "typeclasses.objects.ContainerFeature")) ) def at_object_receive(self, obj, source_location): """ Called when an object enters the room. """ self.check_light_state() def at_object_leave(self, obj, target_location): """ In case people leave with the light. This also works if they are teleported away. """ # do not test for an object going in character's inventory if inherits_from(target_location, Character): # obj.locks.add('view:all()') return # since this hook is called while the object is still in the room, # we exclude it from the light check, to ignore any light sources # it may be carrying. self.check_light_state(exclude=obj) def get_display_name(self, looker, **kwargs): display_name = super().get_display_name(looker, **kwargs) if self.db.is_lit: display_name = fmt_light(display_name) else: display_name = fmt_dark(display_name) return display_name class Zone(DefaultRoom): """ Zones are containers for rooms and, for now, is used to provide path-finding capabilities to mob. """ @classmethod def create(cls, key, account=None, **kwargs): description = kwargs.pop("description", "This is a zone.") size = kwargs.pop("size") size = MAP_SIZE if not size else size zone, errors = super(Zone, cls).create(key, account=account, description=description, **kwargs) if not errors: try: zone.db.size = int(size) except ValueError: errors.append("Size must be an integer.") return zone, errors def at_object_creation(self): super().at_object_creation() self.db.size = MAP_SIZE self.tags.add("zone", category="general") self.locks.add(";".join(["get:false()", "puppet:false()", "view:perm(zone) or perm(Builder)"])) self.at_init() def at_init(self): super().at_init() # when reloaded recalculate path-finding data self._create_paths() def add_room(self, room, x, y): xi = int(x) yi = int(y) if xi < 0 or xi >= self.db.size or yi < 0 or yi >= self.db.size: raise ValueError("Coordinates must be between 0 and {}.".format(self.db.size - 1)) if self.ndb.map[xi][yi]['room_id'] != -1: raise ValueError("Coordinates ({},{}) are not empty.".format(x, y)) room.db.x = xi room.db.y = yi room.db.zone = self.dbref self._add_room_to_graph(room) def remove_room_exit(self, exit_obj): if self.ndb.sp_graph.is_vertex(exit_obj.location) and self.ndb.sp_graph.is_edge(exit_obj.location, exit_obj.destination): self.ndb.sp_graph.del_edge(exit_obj.location, exit_obj.destination) def delete(self): rooms = search_tag(key=self.name, category="zoneId") for room in rooms: room.tags.remove(self.name, category="zoneId") return super().delete() def shortest_path(self, start, end): return spath.shortestPath(self.ndb.sp_graph, start, end) def _create_paths(self): self.ndb.sp_graph = spath.Graph() self.ndb.map = [[{'room_id': -1, 'room_map_icon': "|=g∙|n"} for i in range(self.db.size)] for j in range(self.db.size)] rooms = search_tag(key=self.name, category="zoneId") for room in rooms: self._add_room_to_graph(room) def _add_room_to_graph(self, room): # add to map if 0 <= room.db.x < self.db.size and 0 <= room.db.y < self.db.size and self.ndb.map[room.db.x][room.db.y]["room_id"] == -1: self.ndb.map[room.db.x][room.db.y]['room_id'] = room.dbref self.ndb.map[room.db.x][room.db.y]['room_map_icon'] = room.db.map_icon else: logger.log_err("Cannot add room {} to {} at position {}:{}.".format(room.dbref, self.dbref, room.db.x, room.db.y)) raise Exception("Cannot add room {} to {} at position {}:{}.".format(room.dbref, self.dbref, room.db.x, room.db.y)) # avoid inserting room into graph if is already inserted if not self.ndb.sp_graph.is_vertex(room): self.ndb.sp_graph.add_vertex(room) room.tags.add(self.name, category="zoneId") room.db.zone = self self._update_room_exits(room) def _update_room_exits(self, room): if self.ndb.sp_graph.is_vertex(room): for ex in room.exits: # iterate exits in the room if not self.ndb.sp_graph.is_edge(room, ex.destination): self.ndb.sp_graph.add_edge(room, ex.destination, 1, ex.name) # if destination is already inserted in zone we can proceed, otherwise # aliases are created later. if ex.destination.db.x != - 1 and ex.destination.db.y != - 1: self._update_exit_alias(ex) if ex.db.return_exit: self._update_exit_alias(ex.db.return_exit) def _update_exit_alias(self, exit_obj): xl, yl = exit_obj.location.db.x, exit_obj.location.db.y xd, yd = exit_obj.destination.db.x, exit_obj.destination.db.y # if aliases aren't already created, create them. if not [ele for ele in ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'] if(ele in exit_obj.aliases.all())]: aliases = [] dx = xd - xl dy = yd - yl if dx == 0 and dy == -1: aliases.extend(["n", "north"]) exit_obj.ndb.directional_alias = "n" elif dx == 0 and dy == 1: aliases.extend(["s", "south"]) exit_obj.ndb.directional_alias = "s" elif dx == -1 and dy == 0: aliases.extend(["w", "west"]) exit_obj.ndb.directional_alias = "w" elif dx == 1 and dy == 0: aliases.extend(["e", "east"]) exit_obj.ndb.directional_alias = "e" elif dx == 1 and dy == -1: aliases.extend(["ne", "northeast"]) exit_obj.ndb.directional_alias = "ne" elif dx == -1 and dy == -1: aliases.extend(["nw", "northwest"]) exit_obj.ndb.directional_alias = "nw" elif dx == 1 and dy == 1: aliases.extend(["se", "southeast"]) exit_obj.ndb.directional_alias = "se" elif dx == -1 and dy == 1: aliases.extend(["sw", "southwest"]) exit_obj.ndb.directional_alias = "sw" exit_obj.aliases.add(aliases)