This commit gives an "AI" capability to any object (still triggered by the 'say' command), so now the Wee Beastie can do more than purr. Also fixes the Witch and the Dragon's movements throughout the Realm. We can also Scry into rooms in order to watch the behavior of the Chatbots.
298 lines
9.8 KiB
Python
298 lines
9.8 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
|
|
# Used to keep track of the most recent events:
|
|
short_history = []
|
|
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_full_appearance(self, looker, **kwargs):
|
|
"""
|
|
Return a string containing the full appearance of the
|
|
room, but also the most recent events that have happened.
|
|
"""
|
|
logger.info(f"HISTORY: {self.short_history}")
|
|
return self.return_appearance(looker, **kwargs) + "\n\n" + \
|
|
"\n".join(self.short_history)
|
|
|
|
def get_display_header(self, looker, **kwargs):
|
|
"""
|
|
Possibly send an image to webclients.
|
|
"""
|
|
image = None # 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_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):
|
|
self.store_event(fix_msg(text))
|
|
super().msg_contents(text, exclude, from_obj, mapping,
|
|
raise_funcparse_errors)
|
|
|
|
def store_event(self, text, speaker=None):
|
|
"""
|
|
Store a text message in the short_history.
|
|
"""
|
|
if text and isinstance(text, tuple):
|
|
text = text[0]
|
|
|
|
if speaker:
|
|
text = text.replace("/me", speaker.get_name())
|
|
|
|
self.short_history.append(text.replace(" /", " "))
|
|
self.short_history = self.short_history[-10:]
|
|
|
|
|
|
class DabblersRoom(Room):
|
|
"""
|
|
Change the description based on the state of the fire.
|
|
"""
|
|
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
|