events.t

documentation
#charset "us-ascii"
#include "advlite.h"


/*
 *   *************************************************************************
 *   events.t This module forms an optional part of the adv3Lite library. Note,
 *   however, that it must be present for certain other modules such as actor.t
 *   and scene.t to function properly, so it must be included if any of them
 *   are.
 *
 *   (c) 2012-13 Eric Eve (but based largely on code borrowed from the adv3
 *   library (c) Michael J. Roberts).
 */

/* 
 *   The eventManager is the object that manages the execution of Events such as
 *   Fuses and Daemons.
 */
eventManager: object
    
    /* Add an event to the list of events to be executed. */
    addEvent(event)
    {
        eventList.append(event);
    }
    
    /* Remove an event from the list of events to be executed. */
    removeEvent(event)
    {
        eventList.removeElement(event); 
    }
    
     /* 
     *   Remove events matching the given object and property combination.
     *   We remove all events that match both the object and property
     *   (events matching only the object or only the property are not
     *   affected).
     *   
     *   This is provided mostly as a convenience for cases where an event
     *   is known to be uniquely identifiable by its object and property
     *   values; this saves the caller the trouble of keeping track of the
     *   Event object created when the event was first registered.
     *   
     *   When a particular object/property combination might be used in
     *   several different events, it's better to keep a reference to the
     *   Event object representing each event, and use removeEvent() to
     *   remove the specific Event object of interest.
     *   
     *   Returns true if we find any matching events, nil if not.  
     */
    removeMatchingEvents(obj, prop)
    {
        local found;
        
        /* 
         *   Scan our list, and remove each event matching the parameters.
         *   Note that it's safe to remove things from a vector that we're
         *   iterating with foreach(), since foreach() makes a safe copy
         *   of the vector for the iteration. 
         */
        found = nil;
        foreach (local cur in eventList)
        {
            /* if this one matches, remove it */
            if (cur.eventMatches(obj, prop))
            {
                /* remove the event */
                removeEvent(cur);

                /* note that we found a match */
                found = true;
            }
        }

        /* return our 'found' indication */
        return found;
    }

    /* 
     *   Remove the current event - this is provided for convenience so
     *   that an event can cancel itself in the course of its execution.
     *   
     *   Note that this has no effect on the current event execution -
     *   this simply prevents the event from receiving additional
     *   notifications in the future.  
     */
    removeCurrentEvent()
    {
        /* remove the currently active event from our list */
        removeEvent(curEvent_);
    }
  
     /*
     *   Execute a turn.  We'll execute each fuse and each daemon that is
     *   currently schedulable.  
     */
    executeTurn()
    {
        local lst;
        
        /* 
         *   first execute all the 'Schedulables' that need to run each turn. In
         *   practice this will be all the actor takeTurn routines that will be
         *   registered by the actor module if present.
         */
        executeList(schedulableList);
        
        
        /* 
         *   build a list of all of our events with the current game clock time
         *   - these are the events that are currently schedulable. Also include
         *   any events that have never been executed but whose next run time is
         *   greater than the current turn count (otherwise these events will
         *   never be executed).
         */
        lst = eventList.subset({x: x.getNextRunTime() == libGlobal.totalTurns
                               || (x.executed == nil && x.getNextRunTime() 
                                   && x.getNextRunTime() < libGlobal.totalTurns)
                                   });

        /* execute the items in this list */
        executeList(lst);

        /* no change in scheduling priorities */
        return true;
    }

    /*
     *   Execute a command prompt turn.  We'll execute each
     *   per-command-prompt daemon. 
     */
    executePrompt()
    {
        /* execute all of the per-command-prompt daemons */
        executeList(eventList.subset({x: x.isPromptDaemon}));
    }

    /*
     *   internal service routine - execute the fuses and daemons in the
     *   given list, in eventOrder priority order 
     */
    executeList(lst)
    {
        /* sort the list in ascending event order */
        lst = lst.toList()
              .sort(SortAsc, {a, b: a.eventOrder - b.eventOrder});

        /* run through the list and execute each item ready to run */
        foreach (local cur in lst)
        {
            /* remember our old active event, then establish the new one */
            local oldEvent = curEvent_;
            curEvent_ = cur;

            /* make sure we restore things on the way out */
            try
            {
                /* execute the event */
                cur.executeEvent();
                
                /* note that the event has been executed */
                cur.executed = true;

            }
            catch (Exception exc)
            {
                /* 
                 *   If an event throws an exception out of its handler,
                 *   remove the event from the active list.  If we were to
                 *   leave it active, we'd go back and execute the same
                 *   event again the next time we look for something to
                 *   schedule, and that would in turn probably just
                 *   encounter the same exception - so we'd be stuck in an
                 *   infinite loop executing this erroneous code.  To
                 *   ensure that we don't get stuck, remove the event. 
                 */
                removeCurrentEvent();

                /* re-throw the exception */
                throw exc;
            }
            finally
            {
                /* restore the enclosing current event */
                curEvent_ = oldEvent;
            }
        }
    }

    
    curEvent_ = nil
    
    eventList = static new Vector(20)
    
    /* 
     *   A list of 'schedulables'. These are objects whose executeEvent() method
     *   should be called each turn prior to other events such as Fuses and
     *   Daemons. The main use for this in the library is to provide a mechanism
     *   for the takeTurn method of each Actor to be called before Fuses and
     *   Daemons are run, mainly in case an AgendaItem sets up a Fuse or Daemon
     *   that may need to execute on the same turn, or if a Scene wants to do
     *   so.
     */
    schedulableList = []
;

/* 
 *   An Event is an object such as a Fuse or Daemon that is executed according
 *   to certain conditions. Game code will normally use one of its subclasses
 *   rather than the Event class directly.
 */
class Event: object
   
    /* Construct a new Event */
    construct(obj, prop)
    {
        /* 
         *   Note the object we refer to and the property of that object to
         *   execute.
         */
        obj_ = obj;
        prop_ = prop;
        
        /* Add this Event to the eventManager's list of events/ */
        eventManager.addEvent(self);
    }
    
    /* The object we're associated with */    
    obj_ = nil
    
    /* A pointer to the property of that object to execute */
    prop_ = nil
    
    /* The interval at which this Event is to be executed. */
    interval_ = nil
    
    /* 
     *   Get the next run time, i.e. the next turn on which this Event should
     *   execute obj_.(prop_).
     */
    getNextRunTime()
    {
        return nextRunTime;
    }
    
    /* delay our scheduled run time by the given number of turns */
    delayEvent(turns) { nextRunTime += turns; }
    
     /* 
     *   Execute the event.  This must be overridden by the subclass to
     *   perform the appropriate operation when executed.  In particular,
     *   the subclass must reschedule or unschedule the event, as
     *   appropriate. 
     */
    executeEvent() { }

    /* does this event match the given object/property combination? */
    eventMatches(obj, prop) { return obj == obj_ && prop == prop_; }
    
    /* 
     *   Event order - this establishes the order we run relative to other
     *   events scheduled to run at the same game clock time.  Lowest
     *   number goes first.  By default, we provide an event order of 100,
     *   which should leave plenty of room for custom events before and
     *   after default events.  
     */
    eventOrder = 100
    
    /* 
     *   our next execution time, expressed in game clock time; by
     *   default, we'll set this to nil, which means that we are not
     *   scheduled to execute at all 
     */
    nextRunTime = nil

    /* by default, we're not a per-command-prompt daemon */
    isPromptDaemon = nil   
    
    /* Call the method this Event should execute when it's ready to do so */
    callMethod()
    {
        /* 
         *   If we don't define a senseObj_ or the player character can sense
         *   our senseObj_ via the appropriate sense, simply execute the prop_
         *   method on our obj_. This check allows game code to ensure that the
         *   player doesn't see messages relating to events the player character
         *   cannot perceive.
         */
        if(senseObj_ == nil || Q.(senseProp_)(gPlayerChar, senseObj_))
            obj_.(prop_);
        
        /* Otherwise capture the output from executing obj_.(prop_) */
        else
        {
            captureText = gOutStream.captureOutput({: obj_.(prop_) });
            
            /* 
             *   It's possible that executing the Event changes the sensory
             *   context, so we need to check whether the object in question can
             *   now be sensed, and if so, display the text we've just captured.
             */            
            if(Q.(senseProp_)(gPlayerChar, senseObj_))
                say(captureText);
        }
    }
    
    /* Remove this event from the eventManager's list of events. */
    removeEvent()
    {
        eventManager.removeEvent(self);
    }
    
    /* 
     *   If the senseObj_ property is defined (normally via our constructor),
     *   the player character must be able to sense the senseObj_ via the sense
     *   defined in senseProp_ for any textual output from obj_.(prop_) to be
     *   displayed when this Event is executed.
     */
    senseObj_ = nil
    
    /*   
     *   The sense via which we test whether senseObj_ can be sensed by the
     *   player character. This must be given as an appropriate property of the
     *   Query object, e.g. &canSee or &canHear.
     */
    senseProp_ = nil
    
    /*   Text captured from callMethod() */
    captureText = nil
    
    /*   Flag - has this event ever been executed */
    executed = nil
;

/*
 *   Fuse.  A fuse is an event that fires once at a given time in the
 *   future.  Once a fuse is executed, it is removed from further
 *   scheduling.  
 */
class Fuse: Event
    /* 
     *   Creation.  'turns' is the number of turns in the future at which
     *   the fuse is executed; if turns is 0, the fuse will be executed on
     *   the current turn.  
     */
    construct(obj, prop, interval)
    {
        /* inherit the base class constructor */
        inherited(obj, prop);

        /* 
         *   set my scheduled time to the current game clock time plus the
         *   number of turns into the future 
         */
        nextRunTime = libGlobal.totalTurns + interval;
    }

    /* execute the fuse */
    executeEvent()
    {
        /* call my method */
        callMethod();

        /* a fuse fires only once, so remove myself from further scheduling */
        eventManager.removeEvent(self);
    }
;

/* 
 *   A SenseFuse is just like a Fuse except that any text produced during its
 *   execution is only displayed if the player char is able to sense the
 *   relevant object either at the start or at the end of the Fuse's execution.
 */
class SenseFuse: Fuse
    
    /* 
     *   senseObj is the object which must be sensed for this Fuse's text to be
     *   displayed. senseProp is one of &canSee, &canReach, &canHear, &canSmell.
     *   If these parameters are omitted then the senseObj will be the same as
     *   the obj whose prop property is executed by the Fuse, and the senseProp
     *   will be &canSee, probably the most common case.
     */
    construct(obj, prop, interval,  senseProp = &canSee, senseObj = obj)
    {
        inherited(obj, prop, interval);
        
         senseObj_ = senseObj;
         senseProp_ = senseProp;       
            
    }   
    
;

/*  A Daemon is an Event that executes once every defined number of turns. */
class Daemon: Event
     /*
     *   Creation.  'interval' is the number of turns between invocations
     *   of the daemon; this should be at least 1, which causes the daemon
     *   to be invoked on each turn.  The first execution will be
     *   (interval-1) turns in the future - so if interval is 1, the
     *   daemon will first be executed on the current turn, and if
     *   interval is 2, the daemon will be executed on the next turn.  
     */
    construct(obj, prop, interval)
    {
        /* inherit the base class constructor */
        inherited(obj, prop);

        /* 
         *   an interval of less than 0 is meaningless, so make sure it's
         *   at least 1 
         */
        if (interval < 0)
            interval = 1;

        /* 
         *   remember my interval. In the special case where the interval is 0,
         *   store an interval of 1 but schedule first execution for the current
         *   turn.
         */
        interval_ = interval == 0 ? 1 : interval;

        /* 
         *   set my initial execution time, in game clock time 
         */
//        nextRunTime = libGlobal.totalTurns + interval - 1;
        nextRunTime = libGlobal.totalTurns + interval;
        
        
    }
    
    /* execute the daemon */
    executeEvent()
    {
        /* call my method */
        callMethod();

        /* advance our next run time by our interval */
        nextRunTime += interval_;
    }

    /* our execution interval, in turns */
    interval_ = 1
    
    
;


/* 
 *   A SenseDaemon is just like a Daemon except that any text produced during
 *   its execution is only displayed if the player char is able to sense the
 *   relevant object either at the start or at the end of the Daemon's
 *   execution.
 */
class SenseDaemon: Daemon
    
    /* 
     *   Creation: in addition to the parameters passed to Daemon's constructor,
     *   senseObj is the object which must be sensed for this Daemon's text to
     *   be displayed. senseProp is one of &canSee, &canReach, &canHear,
     *   &canSmell. If these parameters are omitted then the senseObj will be
     *   the same as the obj whose prop property is executed by the Daemon, and
     *   the senseProp will be &canSee, probably the most common case.
     */
    construct(obj, prop, interval, senseProp = &canSee, senseObj = obj)
    {
        inherited(obj, prop, interval);
        
         senseObj_ = senseObj;
         senseProp_ = senseProp;       
            
    }  
    
;

/*
 *   Command Prompt Daemon.  This is a special type of daemon that
 *   executes not according to the game clock, but rather once per command
 *   prompt.  The system executes all of these daemons just before each
 *   time it prompts for a command line.  
 */
class PromptDaemon: Event
    /* execute the daemon */
    executeEvent()
    {
        /* 
         *   call my method - there's nothing else to do for this type of
         *   daemon, since our scheduling is not affected by the game
         *   clock 
         */
        callMethod();
    }

    /* flag: we are a special per-command-prompt daemon */
    isPromptDaemon = true
;

/*
 *   A one-time-only prompt daemon is a regular command prompt daemon,
 *   except that it fires only once.  After it fires once, the daemon
 *   automatically deactivates itself, so that it won't fire again.
 *   
 *   Prompt daemons are occasionally useful for non-recurring processing,
 *   when you want to defer some bit of code until a "safe" time between
 *   turns.  In these cases, the regular PromptDaemon is inconvenient to
 *   use because it automatically recurs.  This subclass is handy for these
 *   cases, since it lets you schedule some bit of processing for a single
 *   deferred execution.
 *   
 *   One special situation where one-time prompt daemons can be handy is in
 *   triggering conversational events - such as initiating a conversation -
 *   at the very beginning of the game.  Initiating a conversation can only
 *   be done from within an action context, but no action context is in
 *   effect during the game's initialization.  An easy way to deal with
 *   this is to create a one-time prompt daemon during initialization, and
 *   then trigger the event from the daemon's callback method.  The prompt
 *   daemon will set up a daemon action environment just before the first
 *   command prompt is displayed, at which point the callback will be able
 *   to trigger the event as though it were in ordinary action handler
 *   code.  
 */
class OneTimePromptDaemon: PromptDaemon
    executeEvent()
    {
        /* execute as normal */
        inherited();

        /* remove myself from the event list, so that I don't fire again */
        removeEvent();
    }
;
Adv3Lite Library Reference Manual
Generated on 03/07/2024 from adv3Lite version 2.1