#charset "us-ascii"
#include "advlite.h"
/*
* SUBTIME EXTENSION
*
* Copyright 2003, 2006 Michael J. Roberts
*
* Adapted for use with adv3Lite by Eric Eve; the changes include using the
* Date class rather than the custom format for storing dates and times used
* in MJR's original version.
*
* "Subjective Time" module. This implements a form of in-game time-keeping
* that attempts to mimic the player's subjective experience of time passing
* in the scenario while still allowing for occasional, reasonably precise
* time readings, such as from a wristwatch in the game world.
*
*
*/
property gameStartTime;
/*
* The clockManager object is used to keep track of subjective time. In this
* version we store dates and times internally as Date objects, although we
* recognize date/times in the form [d, h, m] defined on ClockEvents.
* [DEFINED IN SUBTIME EXTENSION]
*/
clockManager: PreinitObject
/*
* Get the current game-clock time. This returns date/time as a Date
* object.
*
* Remember that our time-keeping scheme is a sort of "Schrodinger's
* clock" [see footnote 1]. Between time checks, the game time clock is
* in a vague, fuzzy state, drifting along at an indeterminate pace from
* the most recent check. When this method is called, though, the clock
* manager is forced to commit to a particular time, because we have to
* give a specific answer to the question we're being asked ("what time is
* it?"). As in quantum mechanics, then, the act of observation affects
* the quantity being observed. Therefore, you should avoid calling this
* routine unnecessarily; call it only when you actually have to tell the
* player what time it is - and don't tell the player what time it is
* unless they ask, or there's some other good reason.
*
* If you want a string-formatted version of the time (as in '9:05pm'),
* you can call checkTimeFmt().
*/
checkTime()
{
local turns;
local mm;
local sdmm;
/*
* Determine how many turns it's been since we last committed to
* a specific wall-clock time. This will give us the
* psychological "scale" of the amount of elapsed wall-clock the
* user might expect.
*/
turns = libGlobal.totalTurns - turnLastCommitted;
/*
* start with the base scaling factor - this is the number of
* minutes of game time we impute to a hundred turns, in the
* absence of the constraint of running up against the next event
*/
mm = (turns * baseScaleFactor) / 100;
/*
* If the base scaled time would take us within sdmm of the next event
* time, slow the clock down from our base scaling factor so that we
* always leave ourselves room to advance the clock further on the
* next check. Reduce the passage of time in proportion to our
* reduced window - so if we have only 60 minutes left, advance time
* at half the normal pace.
*/
if (nextTime != nil)
{
/*
* In Mike Roberts's version of subtime, the clock is slowed down
* if the base scaled time would take us within two hours of the
* next event time, but this may be inappropriate if the game is
* working to a much shorter or longer timescale. So we instead
* compute the time at which slowing down should occur in a
* separate method to make it easy for game code to override it.
* By default, the slow-down time will be half-way between the
* previous event and the next one.
*/
sdmm = slowDownTime();
/* get the minutes between now and the next scheduled event */
local delta = (nextTime - curTime) * 1440;
/* check to see if the raw increment would leave under sdmm minutes */
if (delta - mm < sdmm)
{
/*
* The raw time increment would leave us under sdmm minutes
* away. If we have under sdmm to go before the
* next event, scale down the rate of time in proportion
* to our share under sdmm. (Note that we might
* have more than sdmm to go and still be here,
* because the raw adjusted time leaves under sdmm minutes.)
*/
if (delta < sdmm)
mm = (mm * delta) / sdmm;
/*
* In any case, cap it at half the remaining time, to
* ensure that we won't ever make it to the next event
* time until the next event occurs.
*/
if (mm > delta / 2)
mm = delta / 2;
}
}
/*
* Otherwise, if we have no further events, take the time from the
* timeManager, if it exists.
*/
else if(defined(timeManager))
{
curTime = timeManager.currentTime;
return curTime;
}
/*
* If our calculation has left us with no passage of time, simply
* return the current time unchanged, and do not treat this as a
* commit point. We don't consider this a commit point because
* we treat it as not even checking again - it's effectively just
* a repeat of the last check, since it's still the same time.
* This ensures that we won't freeze the clock for good due to
* rounding - enough additional turns will eventually accumulate
* to nudge the clock forward.
*/
if (mm == 0)
return curTime;
/* add the minutes to the current time */
curTime = curTime.addInterval([0, 0, 0, 0, mm]);
/* the current turn is now the last commit point */
turnLastCommitted = gTurns;
/*
* If the timeManager exists and its time is earlier than the time we
* just calculated, use the timeManager's time.
*/
if(defined(timeManager) && timeManager.currentTime < curTime)
curTime = timeManager.currentTime;
/* If the timeManager exists, synchronize its time with ours. */
syncTime(curTime);
/* return the new time */
return curTime;
}
/*
* Compute the time remaining until the next event at which we start to
* slow down the clock. By default we make this half the time from the
* previous event to the next. Game code can override if some other value
* is preferred. [SUBTIME EXTENSION]
*/
slowDownTime()
{
/*
* If we have a lastEvent, get its eventTime; otherwise assume the
* last event was at our baseDate.
*/
local lastEventTime = lastEvent == nil ? baseDate : lastEvent.eventDate;
/*
* Return half the difference between the next event time and the last
* event time.
*/
return (nextTime - lastEventTime) * 720;
}
/*
* The base scaling factor: this is the number of minutes per hundred
* turns when we have unlimited time until the next event. This number is
* pretty arbitrary, since we're depending so much on the player's
* uncertainty about just how long things take, and also because we'll
* adjust it anyway when we're running out of time before the next event.
* Even so, you might want to adjust this value up or down according to
* your sense of the pacing of your game.
*
* In Mike Roberts's implementation, the baseScaleFactor was defined as a
* constant value of 60. This is still the default value, but if there is
* a next event we take the scale factor from that event's scaleFactor so
* that we can vary the pace of time according to the spacing of events if
* we wish. [SUBTIME EXTENSION]
*/
baseScaleFactor = (nextEvent == nil ? scaleFactor : nextEvent.scaleFactor)
/*
* The scaling factor to use once we have passed the last ClockEvent; this
* is the number of minutes per hundred turns once there are no more
* ClockEvents. [SUBTIME EXTENSION]
*/
scaleFactor = 60
/*
* Get the current game-clock time, formatted into a string with the
* given format mask - see formatTime() for details on how to write a
* mask string.
*
* Note that the same cautions for checkTime() apply here - calling
* this routine commits us to a particular time, so you should call
* this routine only when you're actually ready to display a time to
* the player. [SUBTIME EXTENSION]
*/
checkTimeFmt(fmt) { return formatTime(checkTime(), fmt); }
/*
* Get a formatted version of the given wall-clock time. The time (t)
* parameter can be expressed as a list, in the same format as
* ClockEvent.eventTime: [day,hour,minute], where 'day' is 1 for the first
* day of the game, 2 for the second, and so on. Alternatively it can be
* supplied as a Date object.
*
* The format string (fmt) is specified in the same way as for the
* formatDate() method of the Date class (for which see the TADS 3 System
* Manual). [SUBTIME EXTENSION]
*/
formatTime(t, fmt)
{
/*
* If t has been supplied as a list in the [d, h ,m] format we first
* need to convert it into a Date object
*/
if(dataType(t) == TypeList)
t = eventDate(t);
return t.formatDate(fmt);
}
/* pre-initialize the clockManager [SUBTIME EXTENSION]*/
execute()
{
local vec;
/*
* If the gameMain object defines a gameStartTime, use it to set our
* baseDate.
*/
if(gameMain.propDefined(&gameStartTime))
{
local gst = gameMain.gameStartTime;
if(dataType(gst) == TypeList)
baseDate = new Date(gst[1], gst[2], gst[3]);
if(dataType(gst) == TypeObject && gst.ofKind(Date))
baseDate = gst;
}
/* build a list of all of the ClockEvent objects in the game */
vec = new Vector(10);
forEachInstance(ClockEvent, function(x) {
vec.append(x);
/*
* Fill in the ClockEvent's eventDate property with a Date object
* derived from its eventTime, unless the eventDate has already
* been supplied.
*/
if(x.eventDate == nil)
x.eventDate = eventDate(x.eventTime);
/*
* Otherwise, if a Date has been specified, use it to fill in the
* eventTime property.
*/
else
x.eventTime = getClockTime(x.eventDate);
});
/* sort the list by time */
vec.sort(SortAsc, {a, b: a.eventDate.compareTo(b.eventDate)});
/* store it */
eventList = vec.toList();
/*
* The earliest event is always the marker for the beginning of
* the game. Since it's now the start of the game, mark the
* first event in our list as reached. (The first event is
* always the earliest we find, by virtue of the sort we just
* did.)
*/
vec[1].eventReached();
/*
* If the events module is present, set up a PromptDaemon to check
* each turn whether a new ClockEvent has been reached.
*/
if(defined(eventManager))
new PromptDaemon(self, &reachCheck);
/*
* If the timeManager exists, synchronize its current start time with
* ours, if we have one.
*/
if(defined(timeManager) && lastEvent)
timeManager.currentTime = lastEventDate();
}
/*
* Receive notification from a clock event that an event has just
* occurred. (This isn't normally called directly from game code;
* instead, game code should usually call the ClockEvent object's
* eventReached() method.) [SUBTIME EXTENSION]
*/
eventReached(evt)
{
local idx;
/* find the event in our list */
idx = eventList.indexOf(evt);
/*
* Never go backwards - if events fire out of order, keep only
* the later event. (Games should generally be constructed in
* such a way that events can only fire in order to start with,
* but in case a weird case slips through, we make this extra
* test to ensure that the player doesn't see any strange
* retrograde motion on the clock.)
*/
if (lastEvent != nil && lastEvent.eventDate > evt.eventDate)
return;
/* note the current time */
curTime = evt.eventDate;
/* note the event that's just been reached */
lastEvent = evt;
/*
* if there's another event following, note the next time and the next
* event, and get the next event to calculate its scale factor.
*/
if (idx < eventList.length())
{
nextEvent = eventList[idx + 1];
nextTime = nextEvent.eventDate;
nextEvent.calcScaleFactor();
}
else
{
nextEvent = nil;
nextTime = nil;
}
/*
* we're committing to an exact wall-clock time, so remember the
* current turn counter as the last commit point
*/
turnLastCommitted = gTurns;
/* Note that the event has been reached */
evt.hasBeenReached = true;
/* If the timeManager exists, synchronize its time with ours. */
syncTime(curTime);
}
/*
* Check each turn whether another ClockEvent has been reached. Note that
* this requires the events.t module to be present to work. [SUBTIME EXTENSION]
*/
reachCheck()
{
/* Go through every ClockEvent in our eventList */
foreach(local cur in eventList)
{
/*
* If we find one that hasn't been reached before, but whose
* reachedWhen condition is now true, note that it has now been
* reached.
*/
if(!cur.hasBeenReached && cur.reachedWhen)
{
eventReached(cur);
/*
* Don't continue searching for ClockEvents on this turn; we
* don't want more than one ClockEvent to be reached at a
* time.
*/
break;
}
}
}
/*
* our list of clock events (we build this automatically during
* pre-initialization) [SUBTIME EXTENSION]
*/
eventList = nil
/* the current game clock time [SUBTIME EXTENSION] */
curTime = nil
/* the most recent event that we reached [SUBTIME EXTENSION] */
lastEvent = nil
/* the next event's game clock time [SUBTIME EXTENSION] */
nextTime = nil
/* the next event we're due to reach [SUBTIME EXTENSION] */
nextEvent = nil
/*
* The turn counter (Schedulable.gameClockTime) on the last turn
* where committed to a specific time. Each time we check the time,
* we look here to see how many turns have elapsed since the last
* time check, and we use this to choose a plausible scale for the
* wall-clock time change. [SUBTIME EXTENSION]
*/
turnLastCommitted = 0
/*
* The base date (year, month, day) our game is meant to start on,
* expressed as a Date object. Often this doesn't matter if we're only
* interested in the time of day. By default we make it Jan 1, 2000. If
* gameMain.gameStartDate is defines it will instead be taken from there.
* [SUBTIME EXTENSION]
*/
baseDate = static new Date(2000, 1, 1)
/* Return the date of t as a Date object. [SUBTIME EXTENSION] */
eventDate(t)
{
/* If t is nil, return nil */
if(t == nil)
return nil;
/* Get the date only part of the base date */
local bd = baseDate.getDate();
/* Create a Date object representing midnight on our base date */
local curDate = new Date(bd[1], bd[2], bd[3]);
/*
* Advance this date by the number of days the lastEventTime is from
* the starting time, which is usually day 1
*/
curDate = curDate.addInterval([0, 0, t[1] - 1]);
/* Extract the date part again */
curDate = curDate.getDate();
/* Return the date and time as a Date object */
return new Date(curDate[1], curDate[2], curDate[3], t[2], t[3], 0, 0);
}
/* Retutn the date and time of the last event as a Date object. [SUBTIME EXTENSION] */
lastEventDate()
{
return lastEvent ? eventDate(lastEvent.eventTime) : nil;
}
/* Get the time in our [d, h, m, s] format from a Date object. [SUBTIME EXTENSION] */
getClockTime(dat)
{
/* Get the time element of the Date object */
local t = dat.getHMS();
/* Get the number of days between the Date object and our baseDate */
local days = toInteger(dat - baseDate);
/*
* return the resultant time in [d, h, m] format
*/
return [days + 1, t[1], t[2]];
}
/* Synchronize the timeManager's time with our time, if it exists. [SUBTIME EXTENSION] */
syncTime(t)
{
if(defined(timeManager))
{
/* Set the timeManager's time to our time. */
timeManager.currentTime = t;
/*
* Try to prevent the current turn from advancing the
* timeManager's time beyond our time.
*/
takeTime(0);
}
}
;
/*
* Clock-setting plot event. This object represents a plot point that
* occurs at a particular time in the story world. Create one of these
* for each of your plot events. The Clock Manager automatically builds
* a list of all of these objects during pre-initialization, so you don't
* have to explicitly tell the clock manager about these.
*
* Whenever the story reaches one of these events, you should call the
* eventReached() method of the event object. This will set the clock
* time to the event's current time, and take note of how long we have
* until the next plot event.
* [DEFINED IN SUBTIME EXTENSION]
*/
class ClockEvent: object
/*
* The time at which this event occurs. This is expressed as a list
* with three elements: the day number, the hour (on a 24-hour
* clock), and the minute. The day number is relative to the start
* of the game - day 1 is the first day of the game. So, for
* example, to express 2:40pm on the second day of the game, you'd
* write [2,14,40]. Note that 12 AM is written as 0 (zero) on a
* 24-hour clock, so 12:05am on day 1 would be [1,0,5].
*/
eventTime = [1,0,0]
/* This is the eventTime expressed as a Date object */
eventDate = nil
/* get a formatted version of the event time */
formatTime(fmt) { return clockManager.formatTime(eventTime, fmt); }
/*
* Notify the clock manager that this event has just occurred. This
* sets the game clock to the event's time. The game code must call
* this method when our point in the plot is reached.
*/
eventReached()
{
/* notify the clock manager */
clockManager.eventReached(self);
}
/*
* This is the number of minutes per hundred turns when we have unlimited
* time until this next event. This number is pretty arbitrary, since
* we're depending so much on the player's uncertainty about just how long
* things take, and also because we'll adjust it anyway when we're running
* out of time before the next event. Even so, you might want to adjust
* this value up or down according to your sense of the pacing of your
* game.
*
* Alternatively you can define the turnsToEvent property (see below) and
* the game will calculate an appropriate scaleFactor for you.
* [SUBTIME EXTENSION]
*/
scaleFactor = 60
/*
* Optional: if specified this should contain an estimate of the number of
* turns a player is typically likely to take to get to this event from
* the previous one; the game will then calculate an appropriate
* scaleFactor. Alternatively this can be left at nil and the scaleFactor
* specified directly. [SUBTIME EXTENSION]
*/
turnsToEvent = nil
/*
* If the turnsToEvent property is not nil and the clockManager has
* recorded a previous event, calculate the scaleFactor for this event.
* [SUBTIME EXTENSION]
*/
calcScaleFactor()
{
if(turnsToEvent && clockManager.lastEvent)
{
/*
* Calculate the number of minutes between this event and the
* previous event.
*/
local delta = (eventDate - clockManager.lastEvent.eventDate) * 1440;
/*
* Calculate the scaleFactor such that 100 turns will take
* scaleFactor minutes.
*/
scaleFactor = (delta * 100)/turnsToEvent;
}
}
/*
* A condition (or a method that returns true or nil) that causes this
* event to be reached when it becomes true. This provides an alternative
* way of reaching events (instead of calling the eventReached method).
* [SUBTIME EXTENSION]
*/
reachedWhen = nil
/*
* Flag: has this event been reached? This is used internally by the
* library and shouldn't normally be changed in game code.
* [SUBTIME EXTENSION]
*/
hasBeenReached = nil
;
/* ------------------------------------------------------------------------ */
/*
* [Footnote 1]
*
* "Schrodinger's cat" is a famous thought experiment in quantum
* physics, concerning how a quantum mechanical system exists in
* multiple, mutually exclusive quantum states simultaneously until an
* observer forces the system to assume only one of the states by the
* act of observation. The thought experiment has been popularized as
* an illustration of how weird and wacky QM is, but it's interesting to
* note that Schrodinger actually devised it to expose what he saw as an
* unacceptable paradox in quantum theory.
*
* The thought experiment goes like this: a cat is locked inside a
* special box that's impervious to light, X-rays, etc., so that no one
* on the outside can see what's going on inside. The box contains,
* apart from the cat, a little radiation source and a radiation
* counter. When the counter detects a certain radioactive emission, it
* releases some poison gas, killing the cat. The radioactive emission
* is an inherently quantum mechanical, unpredictable process, and as
* such can (and must) be in a superposition of "emitted" and "not
* emitted" states until observed. Because the whole system is
* unobservable from the outside, the supposition is that everything
* inside is "entangled" with the quantum state of the radioactive
* emission, hence the cat is simultaneously living and dead until
* someone opens the box and checks. It's not just that no one knows;
* rather, the cat is actually and literally alive and dead at the same
* time.
*
* Schrodinger's point was that this superposition of the cat's states
* is a necessary consequence of the way QM was interpreted at the time
* he devised the experiment, but that it's manifestly untrue, since we
* know that cats are macroscopic objects that behave according to
* classical, deterministic physics. Hence a paradox, hence the
* interpretation of the theory must be wrong. The predominant
* interpretation of QM has since shifted a bit so that the cat would
* now count as an observer - not because it's alive or conscious or
* anything metaphysical, but simply because it's macroscopic - so the
* cat's fate is never actually entangled with the radioactive source's
* quantum state. Popular science writers have continued to talk about
* Schrodinger's cat as though it's for real, maybe to make QM seem more
* exotic to laypersons, but most physicists today wouldn't consider the
* experiment to be possible as literally stated. Physicists today
* might think of it as a valid metaphor to decribe systems where all of
* the components are on an atomic or subatomic scale, but no one today
* seriously thinks you can create an actual cat that's simultaneously
* alive and dead.
*/
Adv3Lite Library Reference Manual
Generated on 25/04/2024 from adv3Lite version 2.0