... maybe a bit late, the following code allows you to write a label and register it to a handler.
The handler tracks time (using the store to allow save, rollback, roll forward and load to work properly)
It holds the events, which can be registered anywhere in the script (so, next to each label makes sense)
The menu screen integrates with the handler and automatically creates buttons for available events.
The script is all in one file (you can move parts to other files like screens.rpy if it suits your needs)
It has a few label events to run a game about a thirsty person so you can test (maybe create new project and just copy it into script.rpy)
Anyway...
Code: Select all
######################################
# The Python Class & variables #
######################################
#
# This python stuff could go in a seperate .rpy if you want
#
#
# Initialize this python block in a -1 offset so the
# event_handler object reference is available to inline
# 0 offset python blocks that register labels as events
#
init -1 python:
#
# Import the store to hold the hour variable, using namespace
# to avoid error during instantiation within this -1 block
import renpy.store as ren_store
# default duration for events that do not specify their own
default_event_duration = 1
# the hour the game starts at...
default_start_hour = 0
# The script will actually output the name relevant to
# the number equal or nearest and lower to hour,
# You could name all 24 hours if wanted
# Range would be 0 to 23 (not 1 to 24)
hour_names = ( (0, _("Night Time")),
(8, _("Early Morning")),
(12, _("Noon")),
(18, _("Late Afternoon")),
(22, _("Late Evening"))
)
# Note: Amend the EventHandler.get_hour_of_day() output to alter
# the display of time/date info within the choice menu
# Re-order if you want to start on a different day
weekday_names = ( _("Monday"),
_("Tuesday"),
_("Wednesday"),
_("Thursday"),
_("Friday"),
_("Saturday"),
_("Sunday")
)
# Pretty basic and self explanatory
month_names = ( ( _("November"), range(1,31)), # start day, month days+1
( _("December"), range(1,32))
)
class EventHandler(object):
""" Handle keeping track of time and scheduled events """
def __init__(self):
#
# hour is handled by renpy.store
ren_store.event_handler_hour = default_start_hour
#
# Empty events list to start
self.events = []
def get_str_for_hour(self, hour=None):
""" Ouput a string to represent the hour """
hour = self.get_hour(hour)
ret = "{0} on {1} {2} {3}".format(self.get_hour_name(hour),
self.get_weekday_name(hour),
self.get_month_name(hour),
self.get_day_of_month(hour))
# Comment out this next line to drop showing the hour x:00 part
ret = "{0}:00 - {1}".format(self.get_hour_of_day(), ret)
# Comment out this next line to drop showing the stored hour part
# Only really useful for debugging rollback etc
#
# un-comment it to view total hour count in menu screen
# ret = "({0}) {1}".format(self.get_hour(), ret)
return ret
#
# Setters and Getters to play nicer with rollback
#
def __getattr__(self, name):
if name == 'hour':
return ren_store.event_handler_hour or 0
try:
return self.__dict__[name]
except KeyError:
return type(self).__getattr__(self, name)
def __setattr__(self, name, value):
if name == 'hour':
ren_store.event_handler_hour = value
else:
# We only use this to create the empty events list
self.__dict__[name] = value
def get_hour(self, hour=None):
return hour if hour is not None else ren_store.event_handler_hour
#
# You could theoretically pass a negative to travel back in time ;)
def advance_time(self, hours=1):
# Advance the time by x hours
ren_store.event_handler_hour += hours
#
# Utility functions for working with days, weekdays, day of month
# and months when represented by just an hour value
#
def get_hour_of_day(self, hour=None):
hour = self.get_hour(hour)
return hour % 24
def get_hour_name(self, hour=None):
hour = self.get_hour_of_day(hour)
ret = hour_names[0][1]
for hour_name in hour_names:
if hour < hour_name[0]:
break
ret = hour_name[1]
return ret
def get_day_number(self, hour=None):
hour = self.get_hour(hour)
# day number starting with zero as first day
return divmod(hour, 24)[0]
def get_weekday_name(self, hour=None):
hour = self.get_hour(hour)
return weekday_names[ self.get_weekday_number(hour) ]
def get_weekday_number(self, hour=None):
hour = self.get_hour(hour)
return self.get_day_number(hour) % 7
def get_day_of_month(self, hour=None):
hour = self.get_hour(hour)
day = self.get_day_number(hour) + 1
for month in month_names:
if day <= len(month[1]):
break
day -= len(month[1])
return day
def get_month_name(self, hour=None):
hour = self.get_hour(hour)
return month_names[ self.get_month_number(hour) ][0]
def get_month_number(self, hour=None):
hour = self.get_hour(hour)
day = self.get_day_number(hour)
# remember days start
month_number = 0
for month in month_names:
if day < len(month[1]):
break
month_number += 1
day -= len(month[1])
return month_number
def __str__(self):
return self.get_str_for_hour(self.get_hour())
def add_event(self, label_name=None, **kwargs):
if not isinstance(label_name, basestring) or len(label_name) < 1:
raise KeyError # Must have label name
event_data = {'name' : label_name,
'hour' : range(0, 24), # 0 to 23
'day' : range(1, 32), # 1 to 31
'weekday' : range(0, 7),
'month' : range(0, len(month_names)),
'function' : None,
'data' : {}
}
self.events.append(event_data)
self.update_event(label_name, **kwargs)
def update_event(self, label_name, **kwargs):
event = None
for k in self.events:
if k['name'] == label_name:
event = k
break
if not event:
raise KeyError # Not found
for k in kwargs:
if k == 'name': continue # cannot update label name
elif k in ['hour', 'day', 'weekday', 'month']:
event[k] = self.get_number_range_for(k, kwargs[k])
elif k == 'function':
if globals(kwargs[k]):
event[k] = kwargs[k]
else:
# Unknown keyword, store in data
event['data'][k] = kwargs[k]
def get_number_range_for(self, type, data):
number_range = []
if not isinstance(data, (list, tuple)):
data = [data]
for data_part in data:
if isinstance(data_part, int):
number_range.append(data_part)
elif isinstance(data_part, basestring):
# A string... let's play
type_names = self.get_list_of_names_for(type)
# Try entire string
index = self.get_index_for_name(type_names, data_part)
if index > -1:
number_range.append(index)
elif " to " in data_part:
start, end = data_part.split(" to ")
start_index = self.get_index_for_name(type_names,
start)
end_index = self.get_index_for_name(type_names,
end)
if start_index == -1 or end_index == -1:
raise KeyError
while start_index <= end_index:
number_range.append(start_index)
start_index += 1
if start_index == len(type_names):
start_index = 0
elif isinstance(data_part, (list) ):
# python 2.7 recognizes range as list
for k in data_part:
number_range.append(k)
return number_range
def get_list_of_names_for(self, type='hour'):
if type == 'hour':
ret = [k for k in range(0, 24)]
for hour_name in hour_names:
ret[hour_name[0]] = hour_name[1]
return ret
if type == 'weekday':
return [k for k in weekday_names]
if type == 'month':
return [k[0] for k in month_names]
if type == 'day':
return range(1, 32)
else:
raise KeyError
def get_index_for_name(self, name_list, name):
try:
return int(name)
except:
pass # Silently pass
if name in name_list:
return name_list.index(name)
elif _(name) in name_list:
return name_list.index( _(name) )
return -1
def test_event(self, event, hour=None):
hour = self.get_hour(hour)
# event is a dictionary
if not hour % 24 in event['hour']:
return False
if not self.get_weekday_number(hour) in event['weekday']:
return False
if not self.get_day_of_month(hour) in event['day']:
return False
if not self.get_month_number(hour) in event['month']:
return False
if event['function']:
return globals(event['function'])()
return True
def get_event_data(self, label_name):
for k in self.events:
if k['name'] == label_name:
return k
return False
def get_duration_for_event(self, label_name):
event = self.get_event_data(label_name)
if 'duration' in event['data']:
return event['data']['duration']
return default_event_duration
#
# Utility function to return a ren'py say-able string showing
# the availability and data held against a named label event
#
def get_event_data_str(self, label_name):
ret = "Data for label '{0}': ".format(label_name)
event = self.get_event_data(label_name)
if event:
ret += "Availability: "
for k in ['hour', 'day', 'weekday', 'month']:
ret += "{0} = ".format(k)
if not len(event[k]):
ret += "No data ... "
else:
names = self.get_list_of_names_for(k)
for v in event[k]:
if k != 'day' and names[v]:
v = names[v]
ret += "{0}, ".format(v)
ret = ret[:-2] + " ... "
if ret[-2:] == ". ":
ret = ret[:-5]
else:
ret += "No data"
ret += " --- Function: {0}".format(event['function'])
ret += " --- Data: "
for k in event['data']:
ret += "{0} = {1}, ".format(k, event['data'][k])
if ret[-2:] == ", ":
ret = ret[:-2]
else:
ret += "No data"
else:
ret += "No data found"
return ret
# Find and return the next hour (after passed hour value)
# upon which an event occurs, else -1
def get_next_event_hour(self, hour=None):
hour = self.get_hour(hour)
max_hours = sum(len(k[1]) for k in month_names) * 24
while hour <= max_hours:
hour += 1
for event in self.events:
if self.test_event(event, hour):
return hour
return -1
# How many hours between passed hour value and the next event(s)
def get_hours_until_next_event(self, hour=None):
hour = self.get_hour(hour)
return self.get_next_event_hour(hour) - hour
# Get a list of events available for passed event hour
def get_events_for_hour(self, hour=None):
hour = self.get_hour(hour)
events = []
for event in self.events:
if self.test_event(event, hour):
events.append(event)
return events
# Choice menu actions should call this before any event jump
# to automatically add the duration to the stored hour
def handle_choice_menu_action(self, *args, **kwargs):
duration = 1
if 'data' in kwargs:
if 'duration' in kwargs['data']:
duration = int(kwargs['data']['duration'])
self.advance_time(duration)
renpy.checkpoint()
return None
#
# Instantiate the class
#
event_handler = EventHandler()
#
# End of the Python stuff
#
######################################
# Automated Event Menu #
######################################
#
# Dunno if these styles actually do much
#
# Put these in gui.rpy if you want
#
define gui.event_choice_button_width = gui.choice_button_width
define gui.event_choice_button_height = gui.choice_button_height
define gui.event_choice_button_tile = gui.choice_button_tile
define gui.event_choice_button_borders = gui.choice_button_borders
define gui.event_choice_button_text_font = gui.text_font
define gui.event_choice_button_text_size = gui.text_size
define gui.event_choice_button_text_xalign = 0.5
define gui.event_choice_button_text_idle_color = "#cccccc"
define gui.event_choice_button_text_hover_color = "#ffffff"
define gui.event_choice_text_font = gui.text_font
define gui.event_choice_text_size = gui.text_size
define gui.event_choice_text_xalign = 0.5
#
# Amend and tweak as desired
#
# Put in screens.rpy if you want
#
screen event_choice():
$ events = event_handler.get_events_for_hour()
modal True
style_prefix "event_choice"
vbox:
#
# A String showing the current time/date
#
# Comment it out if desired
$ t = "{0}".format(str(event_handler))
text "[t]"
for event in events:
python:
#
# Build the TextButton text string
#
caption = ""
#
# Check for inline image on left
if 'image_left' in event['data']:
caption = "\{image={0}\} ".format(
event['data']['image_left'])
if 'caption' in event['data']:
caption = "{0}{1}".format(caption,
event['data']['caption'])
else:
# label name with spaces and capitals
name_parts = event['name'].split('_')
for name_part in name_parts:
caption = "{0}{1}{2}".format(
caption,
"" if name_part == name_parts[0] else " ",
name_part.capitalize())
duration = default_event_duration
if 'duration' in event['data']:
duration = int(event['data']['duration'])
#
# We could easily add extra info here saying how long the
# event is going to take
#
# Comment it out if desired
caption = "{0} ({1} hour{2})".format(caption,
duration,
"" if duration == 1 \
else "s")
#
# Finally check for inline image on right
if 'image_right' in event['data']:
caption = "{0} \{image={1}\} ".format(
caption,
event['data']['image_right'])
#
# End Build TextButton text string
textbutton "{0}".format(caption):
action [ Function(event_handler.handle_choice_menu_action, \
data = {'duration': duration}),
Hide("event_choice"),
Jump(event['name'])
]
# Finally a button for passing time
$ next_event_offset = event_handler.get_hours_until_next_event()
if next_event_offset > -1:
textbutton "Wait Until Next Choices in {0} hour{1}".format(
next_event_offset,
"" if next_event_offset == 1 else "s"):
action [ Function(event_handler.handle_choice_menu_action, \
data = {'duration': next_event_offset}),
Hide("event_choice"), #_menu_action, **event),
Jump("just_pass_time")
]
elif not len(events):
#
#
# You might want to change this a bit
#
# Game end button
#
textbutton "Nothing more to do... pass out drunk":
action [ Hide("event_choice"), #_menu_action, **event),
Jump("end_of_game")
]
#
# End of choice menu screen code
#
######################################
# Basic Non Event Labels #
######################################
label start:
"You are horribly thirsty... \
\n... and it is too late to hit a pub."
show screen event_choice
"So, what would you like to do?"
return
label just_pass_time:
show screen event_choice
"Time Passed. What to do now?"
label end_of_game:
$ hours_until_next_event = event_handler.get_hours_until_next_event()
if hours_until_next_event > -1:
show screen event_choice
"Duped by a dodgy rollback... This text should not appear"
"That's all folks!"
return
######################################
# Event Labels #
######################################
#
# Kings Head
#
init python:
event_handler.add_event("drink_at_kings_head",
hour = [20,21],
weekday = "Monday to Wednesday",
duration = 5,
day = range(1, 6),
caption = "Drink at The King's Head",
)
label drink_at_kings_head:
# This would show the event data for named label
$ d = event_handler.get_event_data_str('drink_at_kings_head')
"You drink at the King's Head while reading the debug output... \
\n{size=-7}\n[d]{/size}"
$ duration = event_handler.get_duration_for_event("drink_at_kings_head")
"Sophie serves the drinks. She's cute. [duration] hours pass in a blink"
#
# Inline hour advance (do NOT use in start label though)
$ event_handler.advance_time(1)
"She used her charm to persuade you to stay another hour."
#
show screen event_choice
"Sophie calls last orders. \nWhat to do kangaroo?"
#
# Wier
#
init python:
event_handler.add_event("drink_at_wier",
hour = "20 to 22",
weekday = ["Tuesday", "Thursday"],
day = [1,2, "22 to 26"],
duration = 4,
caption = "Drink at The Wier"
)
label drink_at_wier:
"You drink at The Wier"
$ duration = event_handler.get_duration_for_event("drink_at_wier")
"Kirsty serves the drinks. [duration] hours have passed"
show screen event_choice
"Kirsty calls last orders. \nWhat's the plan pelican?"
#
# Home
#
init python:
event_handler.add_event("drink_at_home",
hour = [19],
weekday = ["Tuesday to Thursday", "Saturday"],
day = range(4,14),
duration = 3,
)
label drink_at_home:
"You drink at home... alone... while playing Ren'py games, yay."
show screen event_choice
"Waiting for a download to finish, you find your glass empty. \
\nWhat now brown cow?"
#
# Midnight Whisky
#
init python:
event_handler.add_event("drink_lemonade",
hour = 0,
weekday = "Monday",
day = range(1,8), # first monday midnight
duration = 1,
)
label drink_lemonade:
"You have a sneaky midnight soda, a nice Lemonade"
show screen event_choice
"That hit the spot. \nTime to sleep and count lil sheep?"
#
# Champagne Xmas
#
init python:
event_handler.add_event("drink_champagne",
hour = range(8, 24),
day = 25,
month = "December",
duration = 3,
)
label drink_champagne:
"You dive into the wine cellar and sit with the Champagne.\
\nSoon you are sipping a glass of bubbly."
show screen event_choice
"Still no sign of Santa. \nWhat's the choice tortoise?"
With a few more tweaks I might add it to the cookbook too.