dkmud/typeclasses/rooms.py
Francesco Cappelli 043cdfc230 commit iniziale.
2022-01-10 14:42:13 +01:00

287 lines
10 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 = 0
self.db.y = 0
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.",
)
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)
"""
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:
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.
"""
def at_object_creation(self):
super().at_object_creation()
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 create_paths(self):
self.ndb.sp_graph = spath.Graph()
self.ndb.map = [[{"room_id": -1} for i in range(MAP_SIZE)] for j in range(MAP_SIZE)]
rooms = search_tag(key=self.name, category="zoneId")
for room in rooms:
self.add_room(room)
def add_room(self, room):
# add to map
if 0 <= room.db.x < MAP_SIZE and 0 <= room.db.y < MAP_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
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)
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)