284 lines
9.4 KiB
Python
284 lines
9.4 KiB
Python
"""
|
|
Custom gametime
|
|
|
|
Contrib - Griatch 2017, vlgeoff 2017
|
|
|
|
This implements the evennia.utils.gametime module but supporting
|
|
a custom calendar for your game world. It allows for scheduling
|
|
events to happen at given in-game times, taking this custom
|
|
calendar into account.
|
|
|
|
Usage:
|
|
|
|
Use as the normal gametime module, that is by importing and using the
|
|
helper functions in this module in your own code. The calendar can be
|
|
specified in your settings file by adding and setting custom values
|
|
for one or more of the variables `TIME_SECS_PER_MIN`,
|
|
`TIME_MINS_PER_HOUR`, `TIME_DAYS_PER_WEEK`, `TIME_WEEKS_PER_MONTH` and
|
|
`TIME_MONTHS_PER_YEAR`. These are all given in seconds and whereas
|
|
they are called "week", "month" etc these names could represent
|
|
whatever fits your game. You can also set `TIME_UNITS` to a dict
|
|
mapping the name of a unit to its length in seconds (like `{"min":
|
|
60, ...}. If not given, sane defaults will be used.
|
|
|
|
"""
|
|
|
|
# change these to fit your game world
|
|
|
|
from django.conf import settings
|
|
from evennia import DefaultScript
|
|
from evennia.utils.create import create_script
|
|
from evennia.utils.gametime import gametime
|
|
# The game time speedup / slowdown relative real time
|
|
TIMEFACTOR = settings.TIME_FACTOR
|
|
|
|
# Game-time units, in game time seconds. These are supplied as a
|
|
# convenient measure for determining the current in-game time, e.g.
|
|
# when defining in-game events. The words month, week and year can be
|
|
# used to mean whatever units of time are used in your game.
|
|
SEC = 1
|
|
MIN = getattr(settings, "TIME_SECS_PER_MIN", 60)
|
|
HOUR = getattr(settings, "TIME_MINS_PER_HOUR", 60) * MIN
|
|
DAY = getattr(settings, "TIME_HOURS_PER_DAY", 24) * HOUR
|
|
WEEK = getattr(settings, "TIME_DAYS_PER_WEEK", 7) * DAY
|
|
MONTH = getattr(settings, "TIME_WEEKS_PER_MONTH", 4) * WEEK
|
|
YEAR = getattr(settings, "TIME_MONTHS_PER_YEAR", 12) * MONTH
|
|
# these are the unit names understood by the scheduler.
|
|
UNITS = getattr(settings, "TIME_UNITS", {
|
|
"sec": SEC,
|
|
"min": MIN,
|
|
"hr": HOUR,
|
|
"hour": HOUR,
|
|
"day": DAY,
|
|
"week": WEEK,
|
|
"month": MONTH,
|
|
"year": YEAR,
|
|
"yr": YEAR,
|
|
})
|
|
|
|
|
|
def time_to_tuple(seconds, *divisors):
|
|
"""
|
|
Helper function. Creates a tuple of even dividends given a range
|
|
of divisors.
|
|
|
|
Args:
|
|
seconds (int): Number of seconds to format
|
|
*divisors (int): a sequence of numbers of integer dividends. The
|
|
number of seconds will be integer-divided by the first number in
|
|
this sequence, the remainder will be divided with the second and
|
|
so on.
|
|
Returns:
|
|
time (tuple): This tuple has length len(*args)+1, with the
|
|
last element being the last remaining seconds not evenly
|
|
divided by the supplied dividends.
|
|
|
|
"""
|
|
results = []
|
|
seconds = int(seconds)
|
|
for divisor in divisors:
|
|
results.append(seconds // divisor)
|
|
seconds %= divisor
|
|
results.append(seconds)
|
|
return tuple(results)
|
|
|
|
|
|
def gametime_to_realtime(format=False, **kwargs):
|
|
"""
|
|
This method helps to figure out the real-world time it will take until an
|
|
in-game time has passed. E.g. if an event should take place a month later
|
|
in-game, you will be able to find the number of real-world seconds this
|
|
corresponds to (hint: Interval events deal with real life seconds).
|
|
|
|
Kwargs:
|
|
format (bool): Formatting the output.
|
|
times (int): The various components of the time (must match UNITS).
|
|
|
|
Returns:
|
|
time (float or tuple): The realtime difference or the same
|
|
time split up into time units.
|
|
|
|
Example:
|
|
gametime_to_realtime(days=2) -> number of seconds in real life from
|
|
now after which 2 in-game days will have passed.
|
|
|
|
"""
|
|
# Dynamically creates the list of units based on kwarg names and UNITs list
|
|
rtime = 0
|
|
for name, value in kwargs.items():
|
|
# Allow plural names (like mins instead of min)
|
|
if name not in UNITS and name.endswith("s"):
|
|
name = name[:-1]
|
|
|
|
if name not in UNITS:
|
|
raise ValueError("the unit {} isn't defined as a valid " \
|
|
"game time unit".format(name))
|
|
rtime += value * UNITS[name]
|
|
rtime /= TIMEFACTOR
|
|
if format:
|
|
return time_to_tuple(rtime, 31536000, 2628000, 604800, 86400, 3600, 60)
|
|
return rtime
|
|
|
|
|
|
def realtime_to_gametime(secs=0, mins=0, hrs=0, days=0, weeks=0,
|
|
months=0, yrs=0, format=False):
|
|
"""
|
|
This method calculates how much in-game time a real-world time
|
|
interval would correspond to. This is usually a lot less
|
|
interesting than the other way around.
|
|
|
|
Kwargs:
|
|
times (int): The various components of the time.
|
|
format (bool): Formatting the output.
|
|
|
|
Returns:
|
|
time (float or tuple): The gametime difference or the same
|
|
time split up into time units.
|
|
|
|
Example:
|
|
realtime_to_gametime(days=2) -> number of game-world seconds
|
|
|
|
"""
|
|
gtime = TIMEFACTOR * (secs + mins * 60 + hrs * 3600 + days * 86400 +
|
|
weeks * 604800 + months * 2628000 + yrs * 31536000)
|
|
if format:
|
|
units = sorted(set(UNITS.values()), reverse=True)
|
|
# Remove seconds from the tuple
|
|
del units[-1]
|
|
|
|
return time_to_tuple(gtime, *units)
|
|
return gtime
|
|
|
|
def custom_gametime(absolute=False):
|
|
"""
|
|
Return the custom game time as a tuple of units, as defined in settings.
|
|
|
|
Args:
|
|
absolute (bool, optional): return the relative or absolute time.
|
|
|
|
Returns:
|
|
The tuple describing the game time. The length of the tuple
|
|
is related to the number of unique units defined in the
|
|
settings. By default, the tuple would be (year, month,
|
|
week, day, hour, minute, second).
|
|
|
|
"""
|
|
current = gametime(absolute=absolute)
|
|
units = sorted(set(UNITS.values()), reverse=True)
|
|
del units[-1]
|
|
return time_to_tuple(current, *units)
|
|
|
|
def real_seconds_until(**kwargs):
|
|
"""
|
|
Return the real seconds until game time.
|
|
|
|
If the game time is 5:00, TIME_FACTOR is set to 2 and you ask
|
|
the number of seconds until it's 5:10, then this function should
|
|
return 300 (5 minutes).
|
|
|
|
Args:
|
|
times (str: int): the time units.
|
|
|
|
Example:
|
|
real_seconds_until(hour=5, min=10, sec=0)
|
|
|
|
Returns:
|
|
The number of real seconds before the given game time is up.
|
|
|
|
"""
|
|
current = gametime(absolute=True)
|
|
units = sorted(set(UNITS.values()), reverse=True)
|
|
# Remove seconds from the tuple
|
|
del units[-1]
|
|
divisors = list(time_to_tuple(current, *units))
|
|
|
|
# For each keyword, add in the unit's
|
|
units.append(1)
|
|
higher_unit = None
|
|
for unit, value in kwargs.items():
|
|
# Get the unit's index
|
|
if unit not in UNITS:
|
|
raise ValueError("unknown unit".format(unit))
|
|
|
|
seconds = UNITS[unit]
|
|
index = units.index(seconds)
|
|
divisors[index] = value
|
|
if higher_unit is None or higher_unit > index:
|
|
higher_unit = index
|
|
|
|
# Check the projected time
|
|
# Note that it can be already passed (the given time may be in the past)
|
|
projected = 0
|
|
for i, value in enumerate(divisors):
|
|
seconds = units[i]
|
|
projected += value * seconds
|
|
|
|
if projected <= current:
|
|
# The time is in the past, increase the higher unit
|
|
if higher_unit:
|
|
divisors[higher_unit - 1] += 1
|
|
else:
|
|
divisors[0] += 1
|
|
|
|
# Get the projected time again
|
|
projected = 0
|
|
for i, value in enumerate(divisors):
|
|
seconds = units[i]
|
|
projected += value * seconds
|
|
|
|
return (projected - current) / TIMEFACTOR
|
|
|
|
def schedule(callback, repeat=False, **kwargs):
|
|
"""
|
|
Call the callback when the game time is up.
|
|
|
|
This function will setup a script that will be called when the
|
|
time corresponds to the game time. If the game is stopped for
|
|
more than a few seconds, the callback may be called with a slight
|
|
delay. If `repeat` is set to True, the callback will be called
|
|
again next time the game time matches the given time. The time
|
|
is given in units as keyword arguments. For instance:
|
|
>>> schedule(func, min=5, sec=0) # Will call next hour at :05.
|
|
>>> schedule(func, hour=2, min=30, sec=0) # Will call the next day at 02:30.
|
|
|
|
Args:
|
|
callback (function): the callback function that will be called [1].
|
|
repeat (bool, optional): should the callback be called regularly?
|
|
times (str: int): the time to call the callback.
|
|
|
|
[1] The callback must be a top-level function, since the script will
|
|
be persistent.
|
|
|
|
Returns:
|
|
The created script (Script).
|
|
|
|
"""
|
|
seconds = real_seconds_until(**kwargs)
|
|
script = create_script("evennia.contrib.custom_gametime.GametimeScript",
|
|
key="GametimeScript", desc="A timegame-sensitive script",
|
|
interval=seconds, start_delay=True,
|
|
repeats=-1 if repeat else 1)
|
|
script.db.callback = callback
|
|
script.db.gametime = kwargs
|
|
return script
|
|
|
|
# Scripts dealing in gametime (use `schedule` to create it)
|
|
class GametimeScript(DefaultScript):
|
|
|
|
"""Gametime-sensitive script."""
|
|
|
|
def at_script_creation(self):
|
|
"""The script is created."""
|
|
self.key = "unknown scr"
|
|
self.interval = 100
|
|
self.start_delay = True
|
|
self.persistent = True
|
|
|
|
def at_repeat(self):
|
|
"""Call the callback and reset interval."""
|
|
callback = self.db.callback
|
|
if callback:
|
|
callback()
|
|
|
|
seconds = real_seconds_until(**self.db.gametime)
|
|
self.restart(interval=seconds)
|