#charset "us-ascii"

#include <adv3.h>
#include <en_us.h>


/*
*   SCENES: A Scene Control Extension
*.  by Eric Eve
*.  Version 1.1; 11-Mar-25
*
*   Thie SCENES Extension provides a means of defining and manipulating
*   Scenes in a way similar to the Scenes mechanism available in Inform 7.
*
*   For a simple Scene, define an object of class Scene, define the
*   condition that starts the Scene in its startsWhen property and the
*   condition that ends the scene in its endsWhen property. Override
*   startUp() and finishOff(howEnded) to define what happens when this
*   scene begins and ends, for example:
*
*.     trainStop: Scene
*.         startsWhen = (gPlayerChar.isIn(platform))
*.         endsWhen = (gRevealed('dinner-date'))
*.
*.         startUp()
*.         {
*.            train.moveInto(platform);
*.            "A train pulls into the station and pulls up at the platform. ";
*.         }
*.
*.         finishOff(howEnded)
*.         {
*.            train.moveInto(nil);
*.            "The train starts moving and accelerates down the track. ";
*.         }
*.     ;
*
*   We can test for whether is scene is currently active by querying its
*   isActive property. We can test whether it has ended by testing
*   trainStop.ended != nil. If it is active we can look at its turnsActive
*   property to see how long it has been active for.
*
*   The trainStop scene defined above will start when the player enters the
*   platform location, and will end when the 'dinner-date' tag is revealed
*   (perhaps as the result of some conversation that takes place on the
*   Platform). By default a Scene is only triggered once, so it won't recur
*   again the next time the player comes to the platform. If you want a
*   scene to recur, set its isRecurring property to true. To see how many
*   times a recurring scene has run, test its timesActive property (which
*   is only updated after the scene has ended, so this will be 1 for a
*   Scene that's in the course of running for the second time).
*
*   We can elaborate this simple Scene definition in a number of ways. Every
*   turn when the Scene is active its daemon() method is called, so we can
*   override this method to do anything we want to happen each turn the
*   scene is active. If we define the Scene to be an EventList as well,
*   then by default the daemon() method will cause its doScript() method,
*   so that all we need to do to make an EventList fire every turn a Scene
*   is active is add the appropriate kind of EventList to the class list of
*   the Scene and define its EventList property.
*
*   We can also arrange for a scene to end in more than one way by returning
*   different non-nil values from its howEnded() method (these methods
*   could be numbers, single-quoted strings, enums or objects, depending
*   what best suits our purpose). The finishOff(howEnded) method can then
*   test its howEnded parameter to see how the scene ended and respond
*   accordingly. If we later want to test how a Scene ended we can inspect
*   the value of is ended property (which will store the value returned by
*   endsWhen).
*
*   A more elaborate form of the trainStop scene incorporating these
*   additional features might look like this:
*
*.     trainStop: Scene, ShuffledEventList
*.         [
*.             new function() {
*.                 laura.moveIntoForTravel(platform);
*.                 "A young woman steps off the train and looks around
*.                     uncertainly. ";
*.             },
*.
*.             {: laura.initiateConversation(lauraPlatformTalkingState,
*.                 'asking-the-time')
*.             }
*.         ]
*.
*.         [
*.             'The train despatcher wanders up and down, raising his whistle
*.              to his lips a few times without actually blowing it. ',
*.
*.             'A couple of passengers run down the platform and leap aboard
*.              the train. ',
*.
*.             'The loudspeaker announces that the 10.15 to London has been
*.              delayed -- yet again. ',
*.
*.             'Several doors slam on the train. '
*.         ]
*.
*.         startsWhen = (gPlayerChar.isIn(platform))
*.
*.         endsWhen()
*.         {
*.             if(gRevealed('dinner-date'))
*.               return 'romantically';
*.
*.             if(gRevealed('faithful-wife'))
*.               return 'sadly-but-wisely';
*.
*.             return nil;
*.         }
*.
*.         startUp()
*.         {
*.            train.moveInto(platform);
*.            "A train pulls into the station and pulls up at the platform. ";
*.         }
*.
*.         finishOff(howEnded)
*.         {
*.            train.moveInto(nil);
*.            "The train starts moving and accelerates down the track. ";
*.            switch(howEnded)
*.            {
*.               case 'romantically':
*.                 "You set off together for a nearby restaurant...";
*.                 gPlayerChar.moveIntoForTravel(restaurant);
*.                 laura.moveIntoForTravel(restaurant);
*.                 break;
*.               case 'sadly-but-wisely':
*.                 "You watch sadly as Laura turns and walks away. ";
*.                  laura.moveInto(nil);
*.                 break;
*.            }
*.         }
*.     ;
*
*   Another way of defining alternative endings is to make the ending types
*   objects and define the finishOff code on those objects. In this case
*   we'd define the endsWhen method like this:
*
*.         endsWhen()
*.         {
*.             if(gRevealed('dinner-date'))
*.               return romanticEnding;
*.
*.             if(gRevealed('faithful-wife'))
*.               return sadButWiseEnding;
*.
*.             return nil;
*.         }
*.
*   Then perhaps define the finishOff(howEnded) method thus:
*.
*.         finishOff(howEnded)
*.         {
*.            train.moveInto(nil);
*.            "The train starts moving and accelerates down the track. ";
*.            inherited(howEnded);  // NOTE THIS CALL TO THE INHERITED METHOD
*.         }
*
*   And finally define our two ending type objects:
*.
*.     romanticEnding: object
*.        finishOff()
*.        {
*.           "You set off together for a nearby restaurant...";
*.            gPlayerChar.moveIntoForTravel(restaurant);
*.            laura.moveIntoForTravel(restaurant);
*.        }
*.      ;
*.
*.    sadButWiseEnding: object
*.        finishOff()
*.        {
*.           "You watch sadly as Laura turns and walks away. ";
*.           laura.moveInto(nil);
*.        }
*.      ;
*
*   Yet another alternative would be to start another scene based on how
*   this scene ended, for example:
*
*.     romanticDinnerScene: Scene
*.       startsWhen = (trainStop.ended == 'romantically')
*.
*.       startUp()
*.       {
*.            You set off together for a nearby restaurant...";
*.            gPlayerChar.moveIntoForTravel(restaurant);
*.            laura.moveIntoForTravel(restaurant);
*.       }
*.       ...
*.     ;
*/


ModuleID
   name = 'Scenes'
   byLine = 'by Eric Eve'
   htmlByLine = 'by <a href="mailto:[email protected]">Eric Eve</a>'
   version = '1.1'
   listingOrder = 70
;


/*
 *   A DaemonControl is a utility class that slightly simplifies the task of
 *   starting and stopping a basic Daemon
 */

class DaemonControl: object
   daemonID = nil

   /*
    *   If startDaemon is called without any arguments, it will start up a
    *   regular Daemon called every turn. If an argument is supplied it
    *   should be a positive integer indicating the interval between
    *   successive invocations of the daemon.
    */

   startDaemon([args])
   {
       local turns = (args.length == 0 ? 1 : args[1]);
       if(daemonID == nil)
           daemonID = new Daemon(self, &daemon, turns);
   }

   /* Stop this Daemon and remove it from the event list */

   stopDaemon()
   {
       if(daemonID != nil)
       {
           stoppingDaemon();
           daemonID.removeEvent();
           daemonID = nil;
       }
   }

   /*
    *   The daemon method is called each turn the Daemon is running (or
    *   each Nth turn if startDaemon() was called as startDaemon(N) ).
    *
    *   By default we call our doScript() method if we're a Script or
    *   EventList.
    */

   daemon()
   {
       if(ofKind(Script))
           doScript();
   }

   /*
    *   The stoppingDaemon is called just before the Daemon is stopped, via
    *   a call to stopDaemon(). By default it does nothing, but we can add
    *   our own code here.
    */

   stoppingDaemon() { }
;

/*
*   a Scene is an abstract object that we can use to mark developments in
*   the plot of our game. Any number of Scenes can be active at once, but
*   the transition from one Scene to another can be used to mark
*   significant developments in ous Story.
*
*   To determine whether a particular Scene is currently active, test its
*   isActive property.
*
*
*   Use the startsWhen and endsWhen methods to set the conditions under
*   which this Scene starts and finishes.
*
*   Override the startUp() method to control what happens when this Scene
*   starts.
*
*   Override the finishOff(howEnded) method to control what happens when
*   this Scene ends. The howEnded parameter is the value returned from the
*   endsEhen method and can be used to make the Scene end in different
*   ways, if we wish.
*
*   Every turn when the Scene is active its daemon() method will be
*   executed; we can use this for anything we want to happen each turn the
*   Scene is active, or we can define the Scene to be some kind of EventList
*   and everyTurn() will drive it.
*/


class Scene: DaemonControl

   /* is this scene currently active? */
   isActive = nil

   /* The number of turns this Scene has been active */
   turnsActive = (libGlobal.totalTurns - startedWhen)

   /* Make this scene active */
   activate()
   {
       /* Note that we're now active */
       isActive = true;

       /*
        *   Note when we started. We subtract one from the turn count since
        *   the turn count will have advanced by one by the time we're
        *   called.
        */
       startedWhen = libGlobal.totalTurns - 1;

       /* Start our associated Daemon */
       startDaemon(daemonInterval);

       /* Call our custom startup code */
       startUp();
   }

   /*  Override this method to define what happens when this scene starts */
   startUp() { }

   /*
    *   The frequency with which our daemon() method is called when we're
    *   active.
    */
   daemonInterval = 1

   /*
    *   End this scene. This can take single parameter that can simply be
    *   true (if the scene just ends with no further specification) or it
    *   can be some kind of value (e.g. an enum, a string, or an object)
    *   indicating how the scene ended; e.g. well or badly. If this argument
    *   is not supplied, a value of true is assumed.
    */
   deactivate(...)
   {
       local howEnded = argcount > 0 ? getArg(1) : true;

       /* Mark us no longer active */
       isActive = nil;

       /* Call our custom finishing off code */
       finishOff(howEnded);

       /* Add to the count of the number of times we've been active */
       timesActive++;

       /* Note how we ended */
       ended = howEnded;

       /* Stop my associated Daemon */
       stopDaemon();

       /* If I'm not needed again, remove me from the list */
       if(!isRecurring)
           sceneController.sceneList.removeElement(self);


   }

   /*
    *   Override this method to define what happens when this scene ends.
    *   This can optionally depend on how the scene ended, as defined by the
    *   how parameter.
    *
    *   A neat scheme for providing alternative endings might be to make the
    *   how parameter an object and have the finishOff(howEnded) method call a
    *   method on the howEnded object. This is implemented as the default,
    *   provided the howEnded parameter is passed as an object; otherwise the
    *   default behaviour is to do nothing.
    */
   finishOff(howEnded)
   {
       if(dataType(howEnded) == TypeObject)
           howEnded.finishOff();
   }

   /*
    *   Override this method with the condition that must be true for this
    *   scene to start.
    */
   startsWhen { return nil; }

   /*
    *   Override this method with the condition that must obtain for this
    *   scene to end. If this scene can end in more this way, return a value
    *   (e.g. an enum, object or string) indicating how the scene ended,
    *   otherwise just return true.
    */
   endsWhen { return nil; }


   /*
    *   Is this a recurring scene? If so, then it will start up each time
    *   startsWhen becomes true. Otherwise (the default) it will start up
    *   only the first time.
    */
   isRecurring = nil

   /*  The number of times this scene has been active */
   timesActive = 0

   /*
    *   The turn count when this scene last started. This can be used, e.g.,
    *   if we want a scene to last a set number of turns.
    */
   startedWhen = 0

   /*
    *   A record of how this scene last ended, if it can end in more than one
    *   way. A value of nil means this scene has never ended. A value of
    *   true means it just ended. Any other value indicates a particular
    *   kind of ending.
    */
   ended = nil

;

/*  Build a list of scenes at Preinit */

scenesPreinit: PreinitObject
   execute()
   {
       forEachInstance(Scene, {x: sceneController.sceneList.append(x) });
   }
;

/* Watch for the opening and closing of scenes each turn */

sceneController: InitObject
   execute()  {   new PromptDaemon(self, &sceneCheck);    }

   /* The list of all scenes in the game */
   sceneList = static new Vector(10)

   sceneCheck()
   {
       local how = nil;

       foreach(local cur in sceneList)
       {

           /* Check if any active scenes are ready to be ended */
           if(cur.isActive && ((how = cur.endsWhen) != nil))
               cur.deactivate(how);

           /* Check if any non-active scenes are due for activation */
           if(!cur.isActive && cur.startsWhen
              && (cur.isRecurring || cur.timesActive == 0))
              cur.activate();

       }
   }
;