subtime.t

documentation
#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 03/07/2024 from adv3Lite version 2.1