376 lines
14 KiB
Python
376 lines
14 KiB
Python
"""
|
|
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)
|
|
|