dkmud/typeclasses/rooms.py
Francesco Cappelli 5ef1b78c42 sistemati spells
2022-02-17 18:33:26 +01:00

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)