#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();
}
}
;