moss-n-puddles/typeclasses/rooms.py
2025-09-14 10:57:19 -07:00

275 lines
9 KiB
Python

"""
Room
Rooms are simple containers that has no location of their own.
"""
from collections import defaultdict
from datetime import datetime
from re import match
from django.utils.translation import gettext as _
from evennia import utils
from evennia.utils import gametime, logger, delay
from evennia.utils.utils import iter_to_str
from evennia.contrib.grid.extended_room import ExtendedRoom
from evennia.contrib.rpg.rpsystem import ContribRPRoom
from django.conf import settings
from typeclasses.pets import Hunger
from typeclasses.objects import ObjectParent, Listener
from utils.word_list import fix_msg
_SEARCH_AT_RESULT = utils.object_from_module(settings.SEARCH_AT_RESULT)
def articlize_character(character, capitalize=False):
"""
Return a simple character with article prefix.
"""
if capitalize:
article = "A"
else:
article = "a"
# Trim off any trailing periods that show up here.
if match(r".*\.$", character):
character = character[:-1]
# logger.info(f"rooms: articlize_character({character})")
if character == "" or match(r"^\|[A-z][A-Z]", character):
return character
elif match(r"^\|[A-z][aeiou]", character):
return f"{article}n " + character
else:
return f"{article} " + character
def articlize_characters(characters):
return ", ".join([articlize_character(c) for c in characters])
def captialize_characters(characters):
"""
Capitalizes and prefixes an article on string of characters.
Assuming one per line.
"""
char_list = characters.split("\n")
updated = [articlize_character(ch, True) for ch in char_list]
return " " + ", ".join(updated)
class Room(ObjectParent, ExtendedRoom, ContribRPRoom, Listener):
"""
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 mygame/typeclasses/objects.py for a list of
properties and methods available on all Objects.
"""
is_dark = False
has_weather = False
appearance_template = """
{header}
|c{name}{extra_name_info}|n
{desc}
{exits}
{things}
{characters}
{footer}
"""
def get_time(self):
"""
Return tuple of current time of this location.
(Minute, Hour, Time of Day, Season)
"""
timestamp = gametime.gametime(absolute=True)
datestamp = datetime.fromtimestamp(timestamp)
return (datestamp.minute, datestamp.hour,
self.get_time_of_day(), self.get_season())
def get_display_header(self, looker, **kwargs):
"""
Possibly send an image to webclients.
"""
image = self.db.title_image
if image:
timed = self.attributes.get("title_time", None)
if timed:
# This makes: mellow-marsh-evening
image = f"{image}-{self.get_time_of_day()}"
season = self.attributes.get("title_season", None)
if season:
# This makes: frog-meadow-winter
image = f"{image}-{self.get_season()}"
level = self.attributes.get("title_level", None)
if level:
# This makes: cozy-house-2
image = f"{image}-{level}"
looker.msg(image=(
f"https://www.howardabrams.com/cozy-players-guide/{image}.jpg",
{"type": "background-pane"}
))
return "|n"
def get_display_footer(self, looker, **kwargs):
return "|n"
def get_display_characters(self, looker, *args, **kwargs):
chars = [c for c in self.contents_get(content_type="character")
if not c.attributes.get('transformed')]
vchars = self.filter_visible(chars, looker, **kwargs)
num_chars = len(vchars)
char_list = iter_to_str(
[articlize_character(char.get_display_name(looker, pose=True, **kwargs))
for char in vchars])
# We add the word 'also' in case we showed there were objects:
objs = self.contents_get(content_type="object")
if len(objs) > 0:
also = 'also '
else:
also = ''
if num_chars > 1:
return f"You {also}see the following characters: {char_list}."
if num_chars > 0:
character = char_list.strip()
if character.startswith('|'):
comp_char = character[2:].lower()
else:
comp_char = character.lower()
if match(r"an? |the ", comp_char):
return f"You {also}see {character}."
elif match(r"[aeiou]", comp_char):
return f"You {also}see an {character}."
else:
return f"You {also}see a {character}."
return ''
def get_display_things(self, looker, *args, **kwargs):
"""
Replace
"""
things = self.filter_visible(self.contents_get(content_type="object"),
looker, **kwargs)
tchars = [c for c in self.contents_get(content_type="character")
if c.attributes.get('transformed')]
grouped_things = defaultdict(list)
for thing in things + tchars:
grouped_things[thing.get_display_name(looker, **kwargs)].append(thing)
thing_names = []
for thingname, thinglist in sorted(grouped_things.items()):
nthings = len(thinglist)
thing = thinglist[0]
singular, plural = thing.get_numbered_name(nthings, looker, key=thingname)
if nthings > 1:
thing_names.append(plural)
elif thing.db.plural:
thing_names.append(thing.get_display_name(looker))
else:
thing_names.append(singular)
thing_names = iter_to_str(thing_names, endsep=_(", and"))
return _(f"You notice {thing_names}." if thing_names else "")
def msg_contents(self, text, exclude=None, from_obj=None, mapping=None,
raise_funcparse_errors=False, **kwargs):
text = fix_msg(text)
super().msg_contents(text, exclude, from_obj, mapping,
raise_funcparse_errors)
class DabblersRoom(Room):
def get_display_desc(self, looker):
fire = self.search("fire")
full_desc = self.db.initial_desc
if fire.hunger() == Hunger.RAVENOUS:
self.db.title_level = 0
full_desc += " " + self.db.fire_out
elif fire.hunger() == Hunger.HUNGRY:
self.db.title_level = 1
full_desc += " " + self.db.fire_dim
elif fire.hunger() == Hunger.FULL:
self.db.title_level = 2
full_desc += " " + self.db.fire_full
else:
self.db.title_level = 3
full_desc += " " + self.db.fire_on
full_desc += " " + self.db.final_desc
return full_desc
class OpenableRoom(Room):
"""
Rooms that accept the "open" and "close" command.
This will move an exit in and out of this location.
Set the follow properties:
@set here/open_exit = $search(red door)
@set here/open_exit_msg = "$You() $conj(open) the door!"
@set here/close_exit_msg = "$You() $conj(close) the door!"
@set here/close_automatic = 240
"""
def do_open(self, opener, item):
"""
Move the 'open_exit' to this location.
"""
the_exit = self.db.open_exit
the_item = self.db.open_item
if the_exit and ((the_item and item.lower() ==the_item.lower()) or
the_exit.key.lower().endswith(item)):
if the_exit.location ==self:
opener.msg(f"The {item} is already open.")
return True
the_exit.location = self
opener.announce_action(self.db.open_exit_msg or
f"$You() $conj(open) the {the_exit.name}")
if self.db.close_automatic:
delay(self.db.close_automatic, self.do_close)
return True
def do_close(self, closer=None, item=None):
"""
Move the 'open_exit' exit to Limbo.
"""
the_exit = self.db.open_exit
# If a user is trying to close an item (an automatic close,
# item will be None), so we just put in this check here:
the_item = self.db.open_item
if item and ((the_item and item.lower() ==the_item.lower()) or
the_exit.key.lower().endswith(item)):
pass
else:
return None
if the_exit:
if the_exit.location == self: # It is opened and can be closed:
the_exit.location = None
if closer:
closer.announce_action(self.db.close_exit_msg or
f"$You() $conj(close) the {the_exit.name}")
else:
self.msg_contents(self.db.close_exit_msg)
elif closer:
closer.msg(f"The {item} is already closed.")
return True