"""
Extended Room
Evennia Contribution - Griatch 2012, vincent-lg 2019, Griatch 2023
This is an extended Room typeclass for Evennia, supporting descriptions that vary
by season, time-of-day or arbitrary states (like burning). It has details, embedded
state tags, support for repeating random messages as well as a few extra commands.
- The room description can be set to change depending on the season or time of day.
- Parts of the room description can be set to change depending on arbitrary states (like burning).
- Details can be added to the room, which can be looked at like objects.
- Alternative text sections can be added to the room description, which will only show if
the room is in a given state.
- Random messages can be set to repeat at a given rate.
Installation/testing:
Adding the `ExtendedRoomCmdset` to the default character cmdset will add all
new commands for use.
In more detail, in mygame/commands/default_cmdsets.py:
```
...
from evennia.contrib import extended_room # <---
class CharacterCmdset(default_cmds.Character_CmdSet):
...
def at_cmdset_creation(self):
...
self.add(extended_room.ExtendedRoomCmdSet) # <---
```
Then, reload to make the new commands available. Note that they only work
on rooms with the `ExtendedRoom` typeclass. Create new rooms with the correct
typeclass or use the `typeclass` command to swap existing rooms.
"""
import datetime
import random
import re
from collections import deque
from django.conf import settings
from django.db.models import Q
from evennia import (
CmdSet,
DefaultRoom,
EvEditor,
FuncParser,
InterruptCommand,
default_cmds,
gametime,
utils,
)
from evennia.typeclasses.attributes import AttributeProperty
from evennia.utils.utils import list_to_string, repeat
# error return function, needed by Extended Look command
_AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1))
# funcparser callable for the ExtendedRoom
[docs]def func_state(roomstate, *args, looker=None, room=None, **kwargs):
"""
Usage: $state(roomstate, text)
Funcparser callable for ExtendedRoom. This is called by the FuncParser when it
returns the description of the room. Use 'default' for a default text when no
other states are set.
Args:
roomstate (str): A roomstate, like "morning", "raining". This is case insensitive.
*args: All these will be combined into one string separated by commas.
Keyword Args:
looker (Object): The object looking at the room. Unused by default.
room (ExtendedRoom): The room being looked at.
Example:
$state(morning, It is a beautiful morning!)
Notes:
We try to merge all args into one text, since this function doesn't require more than one
argument. That way, one may be able to get away without using quotes.
"""
roomstate = str(roomstate).lower()
text = ", ".join(args)
# make sure we have a room and a caller and not something parsed from the string
if not (roomstate and looker and room) or isinstance(looker, str) or isinstance(room, str):
return ""
try:
if roomstate in room.room_states or roomstate == room.get_time_of_day():
return text
if roomstate == "default" and not room.room_states:
# return this if no roomstate is set
return text
except AttributeError:
# maybe used on a non-ExtendedRoom?
pass
return ""
[docs]class ExtendedRoom(DefaultRoom):
"""
An Extended Room
Room states:
A room state is set as a Tag with category "roomstate" and tagkey "on_fire" or "flooded"
etc).
Alternative descriptions:
- Add an Attribute `desc_<roomstate>` to the room, where <roomstate> is the name of the
roomstate to use this for, like `desc_on_fire` or `desc_flooded`. If not given, seasonal
descriptions given in desc_spring/summer/autumn/winter will be used, and last the
regular `desc` Attribute.
Alternative text sections
- Used to add alternative text sections to the room description. These are embedded in the
description by adding `$state(roomstate, txt)`. They will show only if the room is in the
given roomstate. These are managed via the add/remove/get_alt_text methods.
Details:
- This is set as an Attribute `details` (a dict) on the room, with the detail name as key.
When looking at this room, the detail name can be used as a target to look at without having
to add an actual database object for it. The `detail` command is used to add/remove details.
Room messages
- Set `room_message_rate > 0` and add a list of `room_messages`. These will be randomly
echoed to the room at the given rate.
"""
# fallback description if nothing else is set
fallback_desc = "You see nothing special."
# tag room_state category
room_state_tag_category = "room_state"
# time setup
months_per_year = 12
hours_per_day = 24
# seasons per year, given as (start, end) boundaries, each a fraction of a year. These
# will change the description. The last entry should wrap around to the first.
seasons_per_year = {
"spring": (3 / months_per_year, 6 / months_per_year), # March - May
"summer": (6 / months_per_year, 9 / months_per_year), # June - August
"autumn": (9 / months_per_year, 12 / months_per_year), # September - November
"winter": (12 / months_per_year, 3 / months_per_year), # December - February
}
# time-dependent room descriptions (these must match the `seasons_per_year` above).
desc_spring = AttributeProperty("", autocreate=False)
desc_summer = AttributeProperty("", autocreate=False)
desc_autumn = AttributeProperty("", autocreate=False)
desc_winter = AttributeProperty("", autocreate=False)
# time-dependent embedded descriptions, usable as $timeofday(morning, text)
# (start, end) boundaries, each a fraction of a day. The last one should
# end at 0 (not 24) to wrap around to midnight.
times_of_day = {
"night": (0, 6 / hours_per_day), # midnight - 6AM
"morning": (6 / hours_per_day, 12 / hours_per_day), # 6AM - noon
"afternoon": (12 / hours_per_day, 18 / hours_per_day), # noon - 6PM
"evening": (18 / hours_per_day, 0), # 6PM - midnight
}
# normal vanilla description if no other `*_desc` matches or are set.
desc = AttributeProperty("", autocreate=False)
# look-targets without database objects
details = AttributeProperty(dict, autocreate=False)
# messages to send to the room
room_message_rate = 0 # set >0s to enable
room_messages = AttributeProperty(list, autocreate=False)
# Broadcast message
def _get_funcparser(self, looker):
return FuncParser(
{"state": func_state},
looker=looker,
room=self,
)
def _start_broadcast_repeat_task(self):
if self.room_message_rate and self.room_messages and not self.ndb.broadcast_repeat_task:
self.ndb.broadcast_repeat_task = repeat(
self.room_message_rate, self.repeat_broadcast_msg_to_room, persistent=False
)
[docs] def at_init(self):
"""Evennia hook. Start up repeating function whenever object loads into memory."""
self._start_broadcast_repeat_task()
[docs] def start_repeat_broadcast_messages(self):
"""
Start repeating the broadcast messages. Only needs to be called if adding messages
and not having reloaded the server.
"""
self._start_broadcast_repeat_task()
[docs] def repeat_broadcast_message_to_room(self):
"""
Send a message to the room at room_message_rate. By default
we will randomize which one to send.
"""
self.msg_contents(random.choice(self.room_messages))
[docs] def get_time_of_day(self):
"""
Get the current time of day.
Override to customize.
Returns:
str: The time of day, such as 'morning', 'afternoon', 'evening' or 'night'.
"""
timestamp = gametime.gametime(absolute=True)
datestamp = datetime.datetime.fromtimestamp(timestamp)
timeslot = float(datestamp.hour) / self.hours_per_day
for time_of_day, (start, end) in self.times_of_day.items():
if start < end and start <= timeslot < end:
return time_of_day
return time_of_day # final back to the beginning
[docs] def get_season(self):
"""
Get the current season.
Override to customize.
Returns:
str: The season, such as 'spring', 'summer', 'autumn' or 'winter'.
"""
timestamp = gametime.gametime(absolute=True)
datestamp = datetime.datetime.fromtimestamp(timestamp)
timeslot = float(datestamp.month) / self.months_per_year
for season_of_year, (start, end) in self.seasons_per_year.items():
if start < end and start <= timeslot < end:
return season_of_year
return season_of_year # final step is back to beginning
# manipulate room states
@property
def room_states(self):
"""
Get all room_states set on this room.
"""
return list(sorted(self.tags.get(category=self.room_state_tag_category, return_list=True)))
[docs] def add_room_state(self, *room_states):
"""
Set a room-state or room-states to the room.
Args:
*room_state (str): A room state like 'on_fire' or 'flooded'. This will affect
what `desc_*` and `roomstate_*` descriptions/inlines are used. You can add
more than one at a time.
Notes:
You can also set time-based room_states this way, like 'morning' or 'spring'. This
can be useful to force a particular description, but while this state is
set this way, that state will be unaffected by the passage of time. Remove
the state to let the current game time determine this type of states.
"""
self.tags.batch_add(*((state, self.room_state_tag_category) for state in room_states))
[docs] def remove_room_state(self, *room_states):
"""
Remove a roomstate from the room.
Args:
*room_state (str): A roomstate like 'on_fire' or 'flooded'. If the
room did not have this state, nothing happens.You can remove more than one at a time.
"""
for room_state in room_states:
self.tags.remove(room_state, category=self.room_state_tag_category)
[docs] def clear_room_state(self):
"""
Clear all room states.
Note that fallback time-of-day and seasonal states are not affected by this, only
custom states added with `.add_room_state()`.
"""
self.tags.clear(category="room_state")
# control the available room descriptions
[docs] def add_desc(self, desc, room_state=None):
"""
Add a custom description, matching a particular room state.
Args:
desc (str): The description to use when this roomstate is active.
roomstate (str, None): The roomstate to match, like 'on_fire', 'flooded', or "spring".
If `None`, set the default `desc` fallback.
"""
if room_state is None:
self.attributes.add("desc", desc)
else:
self.attributes.add(f"desc_{room_state}", desc)
[docs] def remove_desc(self, room_state):
"""
Remove a custom description.
Args:
room_state (str): The room-state description to remove.
"""
self.attributes.remove(f"desc_{room_state}")
[docs] def all_desc(self):
"""
Get all available descriptions.
Returns:
dict: A mapping of roomstate to description. The `None` key indicates the
base subscription (stored in the `desc` Attribute).
"""
return {
**{None: self.db.desc or ""},
**{
attr.key[5:]: attr.value
for attr in self.db_attributes.filter(db_key__startswith="desc_").order_by("db_key")
},
}
[docs] def get_stateful_desc(self):
"""
Get the currently active room description based on the current roomstate.
Returns:
str: The current description.
Note:
Only one description can be active at a time. Priority order is as follows:
Priority order is as follows:
1. Room-states set by `add_roomstate()` that are not seasons.
If multiple room_states are set, the first one is used, sorted alphabetically.
2. Seasons set by `add_room_state()`. This allows to 'pin' a season.
3. Time-based seasons based on the current in-game time.
4. None, if no seasons are defined in `.seasons_per_year`.
If either of the above is found, but doesn't have a matching `desc_<roomstate>`
description, we move on to the next priority. If no matches are found, the `desc`
Attribute is used.
"""
room_states = self.room_states
seasons = self.seasons_per_year.keys()
seasonal_room_states = []
# get all available descriptions on this room
# note: *_desc is the old form, we support it for legacy
descriptions = dict(
self.db_attributes.filter(
Q(db_key__startswith="desc_") | Q(db_key__endswith="_desc")
).values_list("db_key", "db_value")
)
for roomstate in sorted(room_states):
if roomstate not in seasons:
# if we have a roomstate that is not a season, use it
if desc := descriptions.get(f"desc_{roomstate}") or descriptions.get(
"{roomstate}_desc"
):
return desc
else:
seasonal_room_states.append(roomstate)
if not seasons:
# no seasons defined, so just return the default desc
return self.attributes.get("desc")
for seasonal_roomstate in seasonal_room_states:
# explicit setting of season outside of automatic time keeping
if desc := descriptions.get(f"desc_{seasonal_roomstate}"):
return desc
# no matching room_states, use time-based seasons. Also support legacy *_desc form
season = self.get_season()
if desc := descriptions.get(f"desc_{season}") or descriptions.get(f"{season}_desc"):
return desc
# fallback to normal desc Attribute
return self.attributes.get("desc", self.fallback_desc)
[docs] def replace_legacy_time_of_day_markup(self, desc):
"""
Filter description by legacy markup like `<morning>...</morning>`. Filter
out all such markings that does not match the current time. Supports
'morning', 'afternoon', 'evening' and 'night'.
Args:
desc (str): The unmodified description.
Returns:
str: A possibly modified description.
Notes:
This is legacy. Use the $state markup for new rooms instead.
"""
desc = desc or ""
current_time_of_day = self.get_time_of_day()
# regexes for in-desc replacements (gets cached)
if not hasattr(self, "legacy_timeofday_regex_map"):
timeslots = deque()
for tod in self.times_of_day:
timeslots.append(
(
tod,
re.compile(rf"<{tod}>(.*?)</{tod}>", re.IGNORECASE),
)
)
# map the regexes cyclically, so each one is first once
self.legacy_timeofday_regex_map = {}
for i in range(len(timeslots)):
# mapping {"morning": [morning_regex, ...], ...}
self.legacy_timeofday_regex_map[timeslots[0][0]] = [tup[1] for tup in timeslots]
timeslots.rotate(-1)
# do the replacement
regextuple = self.legacy_timeofday_regex_map[current_time_of_day]
for regex in regextuple:
desc = regex.sub(r"\1" if regex == regextuple[0] else "", desc)
return desc
[docs] def get_display_desc(self, looker, **kwargs):
"""
Evennia standard hook. Dynamically get the 'desc' component of the object description. This
is called by the return_appearance method and in turn by the 'look' command.
Args:
looker (Object): Object doing the looking (unused by default).
**kwargs: Arbitrary data for use when overriding.
Returns:
str: The desc display string.
"""
# get the current description based on the roomstate
desc = self.get_stateful_desc()
# parse for legacy <morning>...</morning> markers
desc = self.replace_legacy_time_of_day_markup(desc)
# apply funcparser
desc = self._get_funcparser(looker).parse(desc, **kwargs)
return desc
# manipulate details
[docs] def add_detail(self, key, description):
"""
This sets a new detail, using an Attribute "details".
Args:
detailkey (str): The detail identifier to add (for
aliases you need to add multiple keys to the
same description). Case-insensitive.
description (str): The text to return when looking
at the given detailkey. This can contain funcparser directives.
"""
if not self.details:
self.details = {} # causes it to be created as real attribute
self.details[key.lower()] = description
set_detail = add_detail # legacy name
[docs] def remove_detail(self, key, *args):
"""
Delete a detail.
Args:
key (str): the detail to remove (case-insensitive).
*args: Unused (backwards compatibility)
The description is only included for compliance but is completely
ignored. Note that this method doesn't raise any exception if
the detail doesn't exist in this room.
"""
self.details.pop(key.lower(), None)
del_detail = remove_detail # legacy alias
[docs] def get_detail(self, key, looker=None):
"""
This will attempt to match a "detail" to look for in the room.
This will do a lower-case match followed by a startsby match. This
is called by the new `look` Command.
Args:
key (str): A detail identifier.
looker (Object, optional): The one looking.
Returns:
detail (str or None): A detail matching the given key, or `None` if
it was not found.
Notes:
A detail is a way to offer more things to look at in a room
without having to add new objects. For this to work, we
require a custom `look` command that allows for `look <detail>`
- the look command should defer to this method on
the current location (if it exists) before giving up on
finding the target.
"""
key = key.lower()
detail_keys = tuple(self.details.keys())
detail = None
if key in detail_keys:
# exact match
detail = self.details[key]
else:
# find closest match starting with key (shortest difference in length)
lkey = len(key)
startswith_matches = sorted(
(
(detail_key, abs(lkey - len(detail_key)))
for detail_key in detail_keys
if detail_key.startswith(key)
),
key=lambda tup: tup[1],
)
if startswith_matches:
# use the matching startswith-detail with the shortest difference in length
detail = self.details[startswith_matches[0][0]]
if detail:
detail = self._get_funcparser(looker).parse(detail)
return detail
return_detail = get_detail # legacy name
# Custom Look command supporting Room details. Add this to
# the Default cmdset to use.
[docs]class CmdExtendedRoomLook(default_cmds.CmdLook):
"""
look
Usage:
look
look <obj>
look <room detail>
look *<account>
Observes your location, details at your location or objects in your vicinity.
"""
[docs] def look_detail(self):
"""
Look for detail on room.
"""
caller = self.caller
if hasattr(self.caller.location, "get_detail"):
detail = self.caller.location.get_detail(self.args, looker=self.caller)
if detail:
caller.location.msg_contents(
f"$You() $conj(look) closely at {self.args}.\n",
from_obj=caller,
exclude=caller,
)
caller.msg(detail)
return True
return False
[docs] def func(self):
"""
Handle the looking.
"""
caller = self.caller
if not self.args:
target = caller.location
if not target:
caller.msg("You have no location to look at!")
return
else:
# search, waiting to return errors so we can also check details
target = caller.search(self.args, quiet=True)
# if there's no target, check details
if not target:
# no target AND no detail means run the normal no-results message
if not self.look_detail():
_AT_SEARCH_RESULT(target, caller, self.args, quiet=False)
return
# otherwise, run normal search result handling
target = _AT_SEARCH_RESULT(target, caller, self.args, quiet=False)
if not target:
return
desc = caller.at_look(target)
# add the type=look to the outputfunc to make it
# easy to separate this output in client.
self.msg(text=(desc, {"type": "look"}), options=None)
# Custom build commands for setting seasonal descriptions
# and detailing extended rooms.
def _desc_load(caller):
return caller.db.eveditor_target.db.desc or ""
def _desc_save(caller, buf):
"""
Save line buffer to the desc prop. This should
return True if successful and also report its status to the user.
"""
roomstates = caller.db.eveditor_roomstates
target = caller.db.eveditor_target
if not roomstates or not hasattr(target, "add_desc"):
# normal description
target.db.desc = buf
elif roomstates:
for roomstate in roomstates:
target.add_desc(buf, room_state=roomstate)
else:
target.db.desc = buf
caller.msg("Saved.")
return True
def _desc_quit(caller):
caller.attributes.remove("eveditor_target")
caller.msg("Exited editor.")
[docs]class CmdExtendedRoomDesc(default_cmds.CmdDesc):
"""
describe an object or the current room.
Usage:
@desc[/switch] [<obj> =] <description>
Switches:
edit - Open up a line editor for more advanced editing.
del - Delete the description of an object. If another state is given, its description
will be deleted.
spring||summer||autumn||winter - room description to use in respective in-game season
<other> - room description to use with an arbitrary room state.
Sets the description an object. If an object is not given,
describe the current room, potentially showing any additional stateful descriptions. The room
states only work with rooms.
Examples:
@desc/winter A cold winter scene.
@desc/edit/summer
@desc/burning This room is burning!
@desc A normal room with no state.
@desc/del/burning
Rooms will automatically change season as the in-game time changes. You can
set a specific room-state with the |wroomstate|n command.
"""
key = "@desc"
switch_options = None
locks = "cmd:perm(desc) or perm(Builder)"
help_category = "Building"
[docs] def parse(self):
super().parse()
self.delete_mode = "del" in self.switches
self.edit_mode = not self.delete_mode and "edit" in self.switches
self.object_mode = "=" in self.args
# all other switches are names of room-states
self.roomstates = [state for state in self.switches if state not in ("edit", "del")]
[docs] def edit_handler(self):
if self.rhs:
self.msg("|rYou may specify a value, or use the edit switch, but not both.|n")
return
if self.args:
obj = self.caller.search(self.args)
else:
obj = self.caller.location or self.msg("|rYou can't describe oblivion.|n")
if not obj:
return
if not (obj.access(self.caller, "control") or obj.access(self.caller, "edit")):
self.caller.msg(f"You don't have permission to edit the description of {obj.key}.")
return
self.caller.db.eveditor_target = obj
self.caller.db.eveditor_roomstates = self.roomstates
# launch the editor
EvEditor(
self.caller,
loadfunc=_desc_load,
savefunc=_desc_save,
quitfunc=_desc_quit,
key="desc",
persistent=True,
)
return
[docs] def show_stateful_descriptions(self):
location = self.caller.location
room_states = location.room_states
season = location.get_season()
time_of_day = location.get_time_of_day()
stateful_descs = location.all_desc()
output = [
f"Room {location.get_display_name(self.caller)} "
f"Season: {season}. Time: {time_of_day}. "
f"States: {', '.join(room_states) if room_states else 'None'}"
]
other_active = False
for state, desc in stateful_descs.items():
if state is None:
continue
if state == season or state in room_states:
output.append(f"Room state |w{state}|n |g(active)|n:\n{desc}")
other_active = True
else:
output.append(f"Room state |w{state}|n:\n{desc}")
active = " |g(active)|n" if not other_active else ""
output.append(f"Room state |w(default)|n{active}:\n{location.db.desc}")
sep = "\n" + "-" * 78 + "\n"
self.caller.msg(sep.join(output))
[docs] def func(self):
caller = self.caller
if not self.args and "edit" not in self.switches and "del" not in self.switches:
if caller.location:
# show stateful descs on the room
self.show_stateful_descriptions()
return
else:
caller.msg("You have no location to describe!")
return
if self.edit_mode:
self.edit_handler()
return
if self.object_mode:
# We are describing an object
target = caller.search(self.lhs)
if not target:
return
desc = self.rhs or ""
else:
# we are describing the current room
target = caller.location or self.msg("|rYou don't have a location to describe.|n")
if not target:
return
desc = self.args
roomstates = self.roomstates
if target.access(self.caller, "control") or target.access(self.caller, "edit"):
if not roomstates or not hasattr(target, "add_desc"):
# normal description
target.db.desc = desc
elif roomstates:
for roomstate in roomstates:
if self.delete_mode:
target.remove_desc(roomstate)
caller.msg(f"The {roomstate}-description was deleted, if it existed.")
else:
target.add_desc(desc, room_state=roomstate)
caller.msg(
f"The {roomstate}-description was set on"
f" {target.get_display_name(caller)}."
)
else:
target.db.desc = desc
caller.msg(f"The description was set on {target.get_display_name(caller)}.")
else:
caller.msg(
"You don't have permission to edit the description "
f"of {target.get_display_name(caller)}."
)
[docs]class CmdExtendedRoomDetail(default_cmds.MuxCommand):
"""
sets a detail on a room
Usage:
@detail[/del] <key> [= <description>]
@detail <key>;<alias>;... = description
Example:
@detail
@detail walls = The walls are covered in ...
@detail castle;ruin;tower = The distant ruin ...
@detail/del wall
@detail/del castle;ruin;tower
This command allows to show the current room details if you enter it
without any argument. Otherwise, sets or deletes a detail on the current
room, if this room supports details like an extended room. To add new
detail, just use the @detail command, specifying the key, an equal sign
and the description. You can assign the same description to several
details using the alias syntax (replace key by alias1;alias2;alias3;...).
To remove one or several details, use the @detail/del switch.
"""
key = "@detail"
locks = "cmd:perm(Builder)"
help_category = "Building"
[docs] def func(self):
location = self.caller.location
if not self.args:
details = location.db.details
if not details:
self.msg(
f"|rThe room {location.get_display_name(self.caller)} doesn't have any"
" details.|n"
)
else:
details = sorted(["|y{}|n: {}".format(key, desc) for key, desc in details.items()])
self.msg("Details on Room:\n" + "\n".join(details))
return
if not self.rhs and "del" not in self.switches:
detail = location.return_detail(self.lhs)
if detail:
self.msg("Detail '|y{}|n' on Room:\n{}".format(self.lhs, detail))
else:
self.msg("Detail '{}' not found.".format(self.lhs))
return
method = "add_detail" if "del" not in self.switches else "remove_detail"
if not hasattr(location, method):
self.caller.msg("Details cannot be set on %s." % location)
return
for key in self.lhs.split(";"):
# loop over all aliases, if any (if not, this will just be
# the one key to loop over)
getattr(location, method)(key, self.rhs)
if "del" in self.switches:
self.caller.msg(f"Deleted detail '{self.lhs}', if it existed.")
else:
self.caller.msg(f"Set detail '{self.lhs}': '{self.rhs}'")
[docs]class CmdExtendedRoomState(default_cmds.MuxCommand):
"""
Toggle and view room state for the current room.
Usage:
@roomstate [<roomstate>]
Examples:
@roomstate spring
@roomstate burning
@roomstate burning (a second time toggles it off)
If the roomstate was already set, it will be disabled. Use
without arguments to see the roomstates on the current room.
"""
key = "@roomstate"
locks = "cmd:perm(Builder)"
help_category = "Building"
[docs] def parse(self):
super().parse()
self.room = self.caller.location
if not self.room or not hasattr(self.room, "room_states"):
self.caller.msg("You have no current location, or it doesn't support room states.")
raise InterruptCommand()
self.room_state = self.args.strip().lower()
[docs] def func(self):
caller = self.caller
room = self.room
room_state = self.room_state
if room_state:
# toggle room state
if room_state in room.room_states:
room.remove_room_state(room_state)
caller.msg(f"Cleared room state '{room_state}' from this room.")
else:
room.add_room_state(room_state)
caller.msg(f"Added room state '{room_state}' to this room.")
else:
# view room states
room_states = list_to_string(
[f"'{state}'" for state in room.room_states] if room.room_states else ("None",)
)
caller.msg(
"Room states (not counting automatic time/season) on"
f" {room.get_display_name(caller)}:\n {room_states}"
)
[docs]class CmdExtendedRoomGameTime(default_cmds.MuxCommand):
"""
Check the game time.
Usage:
time
Shows the current in-game time and season.
"""
key = "time"
locks = "cmd:all()"
help_category = "General"
[docs] def parse(self):
location = self.caller.location
if (
not location
or not hasattr(location, "get_time_of_day")
or not hasattr(location, "get_season")
):
self.caller.msg("No location available - you are outside time.")
raise InterruptCommand()
self.location = location
[docs] def func(self):
location = self.location
season = location.get_season()
timeslot = location.get_time_of_day()
prep = "an" if season == "autumn" else "a"
self.caller.msg(f"It's {prep} {season} day, in the {timeslot}.")
# CmdSet for easily install all commands
[docs]class ExtendedRoomCmdSet(CmdSet):
"""
Groups the extended-room commands.
"""
[docs] def at_cmdset_creation(self):
self.add(CmdExtendedRoomLook())
self.add(CmdExtendedRoomDesc())
self.add(CmdExtendedRoomDetail())
self.add(CmdExtendedRoomState())
self.add(CmdExtendedRoomGameTime())