From 0e9b27cc0166dd363f3410e78996c7c70b44d150 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 26 Jun 2012 01:33:30 +0200 Subject: [PATCH] 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. --- contrib/extended_room.py | 348 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 contrib/extended_room.py diff --git a/contrib/extended_room.py b/contrib/extended_room.py new file mode 100644 index 000000000..c141f9c9d --- /dev/null +++ b/contrib/extended_room.py @@ -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 ... +. 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 " 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"(.*?)", re.IGNORECASE) +RE_AFTERNOON = re.compile(r"(.*?)", re.IGNORECASE) +RE_EVENING = re.compile(r"(.*?)", re.IGNORECASE) +RE_NIGHT = re.compile(r"(.*?)", 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 ... 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 " - 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 + look + look * + + 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] [ =] + @detail[/del] [ = ] + + + Switches for @desc: + spring - set description for 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) +