Contrib: Extended Room typeclass, with time-dependent desc slots (season and time-of-day), as well as a "details" system for adding visual interest or clues to the room wihout having to create new database objects. The extended room is supported by expanded "look" and "@desc" commands.
This commit is contained in:
parent
ca0920e635
commit
0e9b27cc01
1 changed files with 348 additions and 0 deletions
348
contrib/extended_room.py
Normal file
348
contrib/extended_room.py
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
"""
|
||||
Extended Room
|
||||
|
||||
Evennia Contribution - Griatch 2012
|
||||
|
||||
This is an extended Room typeclass for Evennia. It is supported
|
||||
by an extended Look command and an extended @desc command, also
|
||||
in this module.
|
||||
|
||||
|
||||
Features:
|
||||
|
||||
1) Time-changing description slots
|
||||
|
||||
This allows to change the full description text the room shows
|
||||
depending on larger time variations. Four seasons - spring, summer,
|
||||
autumn and winter are used by default). The season is calculated
|
||||
on-demand (no Script or timer needed) and updates the full text block.
|
||||
|
||||
There is also a general description which is used as fallback if
|
||||
one or more of the seasonal descriptions are not set when their
|
||||
time comes.
|
||||
|
||||
An updated @desc command allows for setting seasonal descriptions.
|
||||
|
||||
|
||||
2) In-description changing tags
|
||||
|
||||
Within each seasonal (or general) description text, you can also embed
|
||||
time-of-day dependent sections. Text inside such a tag will only show
|
||||
during that particular time of day. The tags looks like <timeslot> ...
|
||||
</timeslot>. By default there are four timeslots per day - morning,
|
||||
afternoon, evening and night.
|
||||
|
||||
|
||||
3) Details
|
||||
|
||||
The Extended Room can be "detailed" with special keywords. This makes
|
||||
use of a special Look command. Details are "virtual" targets to look
|
||||
at, without there having to be a database object created for it. The
|
||||
Details are simply stored in a dictionary on the room and if the look
|
||||
command cannot find an object match for a a "look <target>" command it
|
||||
will also look through the available details at the current location
|
||||
if applicable. An extended @desc command is used to set details.
|
||||
|
||||
|
||||
Installation:
|
||||
|
||||
1) Add CmdExtendedLook and CmdExtendedDesc from this module to the default cmdset (see wiki how to do this).
|
||||
2) @dig a room of type contrib.extended_room.ExtendedRoom (or make it the default room type)
|
||||
3) Use @desc and @detail to customize the room, then play around!
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
from django.conf import settings
|
||||
from ev import Room
|
||||
from ev import gametime
|
||||
from ev import default_cmds
|
||||
from ev import utils
|
||||
|
||||
# error return function, needed by Extended Look command
|
||||
_AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1))
|
||||
|
||||
# regexes for in-desc replacements
|
||||
RE_MORNING = re.compile(r"<morning>(.*?)</morning>", re.IGNORECASE)
|
||||
RE_AFTERNOON = re.compile(r"<afternoon>(.*?)</afternoon>", re.IGNORECASE)
|
||||
RE_EVENING = re.compile(r"<evening>(.*?)</evening>", re.IGNORECASE)
|
||||
RE_NIGHT = re.compile(r"<night>(.*?)</night>", re.IGNORECASE)
|
||||
# this map is just a faster way to select the right regexes (the first
|
||||
# regex in each tuple will be parsed, the following will always be weeded out)
|
||||
REGEXMAP = {"morning": (RE_MORNING, RE_AFTERNOON, RE_EVENING, RE_NIGHT),
|
||||
"afternoon": (RE_AFTERNOON, RE_MORNING, RE_EVENING, RE_NIGHT),
|
||||
"evening": (RE_EVENING, RE_MORNING, RE_AFTERNOON, RE_NIGHT),
|
||||
"night": (RE_NIGHT, RE_MORNING, RE_AFTERNOON, RE_EVENING)}
|
||||
|
||||
# set up the seasons and time slots. This assumes gametime started at the
|
||||
# beginning of the year (so month 1 is equivalent to January), and that
|
||||
# one CAN divive the game's year into four seasons in the first place ...
|
||||
MONTHS_PER_YEAR = settings.TIME_MONTH_PER_YEAR
|
||||
SEASONAL_BOUNDARIES = (3/12.0, 6/12.0, 9/12.0)
|
||||
HOURS_PER_DAY = settings.TIME_HOUR_PER_DAY
|
||||
DAY_BOUNDARIES = (0, 6/24.0, 12/24.0, 18/24.0)
|
||||
|
||||
# implements the Extended Room
|
||||
|
||||
class ExtendedRoom(Room):
|
||||
"""
|
||||
This room implements a more advanced look functionality depending on time. It also
|
||||
allows for "details", together with a slightly modified look command.
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called when room is first created only."
|
||||
self.db.spring_desc = ""
|
||||
self.db.summer_desc = ""
|
||||
self.db.autumn_desc = ""
|
||||
self.db.winter_desc = ""
|
||||
# the general desc is used as a fallback if a given seasonal one is not set
|
||||
self.db.general_desc = "This is an extended room."
|
||||
self.db.raw_desc = "" # will be set dynamically. Can contain raw timeslot codes
|
||||
self.db.desc = "" # this will be set dynamically at first look. Parsed for timeslot codes
|
||||
# these will be filled later
|
||||
self.ndb.last_season = None
|
||||
self.ndb.last_timeslot = None
|
||||
# detail storage
|
||||
self.db.details = {}
|
||||
|
||||
def get_time_and_season(self):
|
||||
"""
|
||||
Calcualte the current time and season ids
|
||||
"""
|
||||
# get the current time as parts of year and parts of day
|
||||
time = gametime.gametime(format=True) # returns a tuple (years,months,weeks,days,hours,minutes,sec)
|
||||
month, hour = time[1], time[4]
|
||||
season = float(month) / MONTHS_PER_YEAR
|
||||
timeslot = float(hour) / HOURS_PER_DAY
|
||||
|
||||
# figure out which slots these represent
|
||||
if SEASONAL_BOUNDARIES[0] <= season < SEASONAL_BOUNDARIES[1]: curr_season = "spring"
|
||||
elif SEASONAL_BOUNDARIES[1] <= season < SEASONAL_BOUNDARIES[2]: curr_season = "summer"
|
||||
elif SEASONAL_BOUNDARIES[2] <= season < SEASONAL_BOUNDARIES[3]: curr_season = "autumn"
|
||||
else: curr_season = "winter"
|
||||
|
||||
if DAY_BOUNDARIES[0] <= timeslot < DAY_BOUNDARIES[1]: curr_timeslot = "night"
|
||||
elif DAY_BOUNDARIES[1] <= timeslot < DAY_BOUNDARIES[2]: curr_timeslot = "morning"
|
||||
elif DAY_BOUNDARIES[2] <= timeslot < DAY_BOUNDARIES[3]: curr_timeslot = "afternoon"
|
||||
else: curr_timeslot = "evening"
|
||||
|
||||
print "season:%s, timeslot:%s" % (curr_season, curr_timeslot)
|
||||
return curr_season, curr_timeslot
|
||||
|
||||
def replace_timeslots(self, raw_desc, curr_time):
|
||||
"""
|
||||
Filter so that only time markers <timeslot>...</timeslot> of the correct timeslot
|
||||
remains in the description.
|
||||
"""
|
||||
regextuple = REGEXMAP[curr_time]
|
||||
raw_desc = regextuple[0].sub(r"\1", raw_desc)
|
||||
raw_desc = regextuple[1].sub("", raw_desc)
|
||||
raw_desc = regextuple[2].sub("", raw_desc)
|
||||
return regextuple[3].sub("", raw_desc)
|
||||
|
||||
def return_detail(self, key):
|
||||
"""
|
||||
This will attempt to match a "detail" to look for in the room. 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.
|
||||
|
||||
Details are not season-sensitive, but are parsed for timeslot markers.
|
||||
"""
|
||||
detail = self.db.details.get(key.lower(), None)
|
||||
if detail:
|
||||
season, timeslot = self.get_time_and_season()
|
||||
detail = self.replace_timeslots(detail, timeslot)
|
||||
return detail
|
||||
return None
|
||||
|
||||
def return_appearance(self, looker):
|
||||
"This is called when e.g. the look command wants to retrieve the description of this object."
|
||||
raw_desc = self.db.raw_desc
|
||||
update = False
|
||||
|
||||
# get current time and season
|
||||
curr_season, curr_timeslot = self.get_time_and_season()
|
||||
|
||||
# compare with previously stored slots
|
||||
last_season = self.ndb.last_season
|
||||
last_timeslot = self.ndb.last_timeslot
|
||||
|
||||
if curr_season != last_season:
|
||||
# season changed. Load new desc, or a fallback.
|
||||
if curr_season == 'spring': new_raw_desc = self.db.spring_desc
|
||||
elif curr_season == 'summer': new_raw_desc = self.db.summer_desc
|
||||
elif curr_season == 'autumn': new_raw_desc = self.db.autumn_desc
|
||||
else: new_raw_desc = self.db.winter_desc
|
||||
if new_raw_desc:
|
||||
raw_desc = new_raw_desc
|
||||
else:
|
||||
# no seasonal desc set. Use fallback
|
||||
raw_desc = self.db.general_desc
|
||||
self.db.raw_desc = raw_desc
|
||||
self.ndb.last_season = curr_season
|
||||
update = True
|
||||
|
||||
if curr_timeslot != last_timeslot:
|
||||
# timeslot changed. Set update flag.
|
||||
self.ndb.last_timeslot = curr_timeslot
|
||||
update = True
|
||||
|
||||
if update:
|
||||
# if anything changed we have to re-parse the raw_desc for time markers
|
||||
# and re-save the description again.
|
||||
self.db.desc = self.replace_timeslots(raw_desc, curr_timeslot)
|
||||
# run the normal return_appearance method, now that desc is updated.
|
||||
return super(ExtendedRoom, self).return_appearance(looker)
|
||||
|
||||
|
||||
# Custom Look command supporting Room details. Add this to the Default cmdset to use.
|
||||
|
||||
class CmdExtendedLook(default_cmds.CmdLook):
|
||||
"""
|
||||
look
|
||||
|
||||
Usage:
|
||||
look
|
||||
look <obj>
|
||||
look <room detail>
|
||||
look *<player>
|
||||
|
||||
Observes your location, details at your location or objects in your vicinity.
|
||||
"""
|
||||
def func(self):
|
||||
"""
|
||||
Handle the looking - add fallback to details.
|
||||
"""
|
||||
caller = self.caller
|
||||
args = self.args
|
||||
if args:
|
||||
looking_at_obj = caller.search(args, use_nicks=True, ignore_errors=True)
|
||||
if not looking_at_obj:
|
||||
# no object found. Check if there is a matching detail at location.
|
||||
location = caller.location
|
||||
if location and hasattr(location, "return_detail") and callable(location.return_detail):
|
||||
detail = location.return_detail(args)
|
||||
if detail:
|
||||
# we found a detail instead. Show that.
|
||||
caller.msg(detail)
|
||||
return
|
||||
# no detail found. Trigger delayed error messages
|
||||
_AT_SEARCH_RESULT(caller, args, looking_at_obj, False)
|
||||
return
|
||||
else:
|
||||
# we need to extract the match manually.
|
||||
looking_at_obj = looking_at_obj[0]
|
||||
else:
|
||||
looking_at_obj = caller.location
|
||||
if not looking_at_obj:
|
||||
caller.msg("You have no location to look at!")
|
||||
return
|
||||
|
||||
if not hasattr(looking_at_obj, 'return_appearance'):
|
||||
# this is likely due to us having a player instead
|
||||
looking_at_obj = looking_at_obj.character
|
||||
if not looking_at_obj.access(caller, "view"):
|
||||
caller.msg("Could not find '%s'." % args)
|
||||
return
|
||||
# get object's appearance
|
||||
caller.msg(looking_at_obj.return_appearance(caller))
|
||||
# the object's at_desc() method.
|
||||
looking_at_obj.at_desc(looker=caller)
|
||||
|
||||
|
||||
# Custom build commands for setting seasonal descriptions and detailing extended rooms.
|
||||
|
||||
class CmdExtendedDesc(default_cmds.CmdDesc):
|
||||
"""
|
||||
@desc - describe an object or room
|
||||
|
||||
Usage:
|
||||
@desc[/switch] [<obj> =] <description>
|
||||
@detail[/del] [<key> = <description>]
|
||||
|
||||
|
||||
Switches for @desc:
|
||||
spring - set description for <season> in current room
|
||||
summer
|
||||
autumn
|
||||
winter
|
||||
|
||||
Switch for @detail:
|
||||
del - delete a named detail
|
||||
|
||||
Sets the "desc" attribute on an object. If an object is not given,
|
||||
describe the current room.
|
||||
|
||||
The alias @detail allows to assign a "detail" (a non-object
|
||||
target for the look command) to the current room.
|
||||
"""
|
||||
aliases = ["@describe", "@detail"]
|
||||
|
||||
def func(self):
|
||||
"Define extended command"
|
||||
caller = self.caller
|
||||
if self.cmdstring == '@detail':
|
||||
# switch to detailing mode. This operates only on current location
|
||||
location = self.caller.location
|
||||
if not location:
|
||||
caller.msg("No location to detail!")
|
||||
return
|
||||
if not self.rhs:
|
||||
# no '=' used - list content of given detail
|
||||
if self.args in location.db.details:
|
||||
string = "{wDetail '%s' on %s:\n{n" % (self.args, location)
|
||||
string += location.db.details[self.args]
|
||||
caller.msg(string)
|
||||
return
|
||||
if not self.args:
|
||||
# No args given. Return all details on location
|
||||
string = "{wDetails on %s{n:\n" % location
|
||||
string += "\n".join(" {w%s{n: %s" % (key, utils.crop(text)) for key, text in location.db.details.items())
|
||||
caller.msg(string)
|
||||
return
|
||||
if self.switches and self.switches[0] in 'del':
|
||||
# removing a detail.
|
||||
if self.lhs in location.db.details:
|
||||
del location.db.detail
|
||||
caller.msg("Detail %s deleted, if it existed." % self.lhs)
|
||||
return
|
||||
|
||||
# setting a detail
|
||||
location.db.details[self.lhs] = self.rhs
|
||||
caller.msg("Set Detail %s to '%s'." % (self.lhs, self.rhs))
|
||||
return
|
||||
else:
|
||||
# we are doing a @desc call
|
||||
if self.switches and self.switches[0] in ("spring", "summer", "autumn", "winter"):
|
||||
# a seasonal switch was given
|
||||
if self.rhs:
|
||||
caller.msg("Seasonal descs only works with rooms, not objects.")
|
||||
return
|
||||
switch = self.switches[0]
|
||||
location = caller.location
|
||||
if not location:
|
||||
caller.msg("No location was found!")
|
||||
return
|
||||
if switch == 'spring': location.db.spring_desc = self.args
|
||||
elif switch == 'summer': location.db.summer_desc = self.args
|
||||
elif switch == 'autumn': location.db.autumn_desc = self.args
|
||||
elif switch == 'winter': location.db.winter_desc = self.args
|
||||
# clear flag to force an update
|
||||
location.ndb.last_season = None
|
||||
caller.msg("Seasonal description was set on %s." % location.key)
|
||||
elif self.rhs:
|
||||
# Not a seasonal desc, and we have an =
|
||||
obj = caller.search(self.lhs)
|
||||
if not obj:
|
||||
return
|
||||
obj.db.desc = self.rhs
|
||||
caller.msg("The description was set on %s." % obj.key)
|
||||
else:
|
||||
# set a normal non-seasonal description (fallback) on room
|
||||
obj = caller.location
|
||||
obj.db.general_desc = self.args
|
||||
obj.db.desc = self.args # compatability
|
||||
caller.msg("General description was set on %s." % obj.key)
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue