Skip to main content

News

Show Posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.

Topics - Fat Cerberus

46
There used to be a script that would dynamically change the layer of persons on a map to make them render in front of or behind objects depending on where they are in relation to it.  I wanted to try it out in minisphere as a test case for map engine compatibility, but I don't remember the name of it and nothing like that appears to be in the Downloads repo.  I originally thought it was Lithonite, but that turned out to be something different and WAY more advanced. :)  Anyone know the script I'm talking about?
47
So, I use the Sphere source as reference material in order to keep minisphere's compatibility as high as possible, and I found this gem in the map loading logic:

Code: (cpp) [Select]
inline std::string ReadMapString(IFile* file)
{
    std::string s;
    word length = ReadMapWord(file);

    for (int i = 0; i < length; i++)
    {
        char c;
        file->Read(&c, 1);
        s += c;
    }
    return s;
}


It's the function that reads lstrings from an RMP map file.  Note that it reads the string one character at a time. :o

Sorry for the spam, just found this amusing and had to share it!

edit - changed code tag to syntax highlight +neo
48
According to the internal RTS spec on GitHub here...
https://github.com/sphere-group/sphere-doc/blob/master/internal/tileset.rts.txt

...the obstruction data area is apparently obsolete.  Is this true?  If so I can avoid loading the obstruction section entirely, but then how are obstructed tiles specified now?

Then again, the RMP documentation says the tileset filename string is obsolete also, when it's clearly not (Sphere Studio writes this string and the engine rightly honors it), so... I'm confused. ???
49
Programming / What's wrong here?
Okay, I'm completely stumped here.  As it turns out, the Spectacles threader doesn't run on minisphere.  When it comes time to render or update threads, I get errors because, for some reason, the thread objects are being scrambled and some are coming back undefined.  At first I suspected a Duktape bug, but as it turns out, guess what?  The same exact thing happens, in the same exact place, when run in Sphere-SFML!  This obviously isn't a coincidence; SpiderMonkey in Sphere 1.5 implements ES3 while both Duktape and Jurassic are ES5-compliant.  Meaning there's something in there that's screwing up under ES5, but I can't figure out what, and I've combed the code over a dozen times.

Maybe somebody else could figure it out?  Here's the source for the Specs threader:

Code: (javascript) [Select]
/***
* Specs Engine v6: Spectacles Saga Game Engine
  *           Copyright (C) 2012 Power-Command
***/

// Threads object
// Represents the Specs Engine thread manager.
Threads = new (function()
{
this.hasUpdated = false;
this.nextThreadID = 1;
this.threads = [];
})();

// .initialize() method
// Initializes the thread manager.
Threads.initialize = function()
{
this.threadSorter = function(a, b) {
return a.priority != b.priority ?
a.priority - b.priority :
a.id - b.id;
};
}

// .create() method
// Creates a thread and begins running it.
// Arguments:
//     o:            The object to pass as 'this' to the updater/renderer/input handler. May be null.
//     updater:      The update function for the new thread.
//     renderer:     Optional. The render function for the new thread.
//     priority:     Optional. The render priority for the new thread. Higher-priority threads are rendered
//                   later in a frame than lower-priority ones. Ignored if no renderer is provided. (default: 0)
//     inputHandler: Optional. The input handler for the new thread.
Threads.create = function(o, updater, renderer, priority, inputHandler)
{
renderer = renderer !== void null ? renderer : null;
inputHandler = inputHandler !== void null ? inputHandler : null;
priority = priority !== void null ? priority : 0;

updater = updater.bind(o);
renderer = renderer !== null ? renderer.bind(o) : null;
inputHandler = inputHandler !== null ? inputHandler.bind(o) : null;
var newThread = {
id: this.nextThreadID,
inputHandler: inputHandler,
isUpdating: false,
priority: priority,
renderer: renderer,
updater: updater
};
this.threads.push(newThread);
++this.nextThreadID;
return newThread.id;
};

// .createEntityThread() method
// Creates a thread for a specified entity.
// Arguments:
//     entity:   The entity for which to create the thread. This should be an object having .update() and
//               optionally, .render() and .getInput() methods. Each of these will be called once
//               per frame until the thread either finishes (entity.update() returns false) or is terminated.
//     priority: Optional. The render priority for the new thread. Higher-priority threads are rendered
//               later in a frame than lower-priority ones. Ignored if no renderer is provided. (default: 0)
Threads.createEntityThread = function(entity, priority)
{
priority = priority !== void null ? priority : 0;

var updater = entity.update;
var renderer = (typeof entity.render === 'function') ? entity.render : null;
var inputHandler = (typeof entity.getInput === 'function') ? entity.getInput : null;
return this.create(entity, updater, renderer, priority, inputHandler);
};

// .doFrame() method
// Performs update and render processing for a single frame.
Threads.doFrame = function()
{
if (IsMapEngineRunning()) {
RenderMap();
} else {
this.renderAll();
}
FlipScreen();
if (IsMapEngineRunning()) {
this.hasUpdated = false;
UpdateMapEngine();
if (!this.hasUpdated) {
this.updateAll();
}
} else {
this.updateAll();
}
};

// .doWith() method
// Creates an improvised thread running in the context of a specified object.
// Arguments:
//     o:            The object to pass as 'this' to the updater/renderer.
//     updater:      The update function for the new thread.
//     renderer:     Optional. The render function for the new thread.
//     priority:     Optional. The render priority for the new thread. Higher-priority threads are rendered
//                   later in a frame (closer to the player) than lower-priority ones. (default: 0)
//     inputHandler: Optional. The input handler for the new thread.
Threads.doWith = function(o, updater, renderer, priority, inputHandler)
{
return this.create(o, updater, renderer, priority, inputHandler);
};

// .isRunning() method
// Determines whether a thread is still running.
// Arguments:
//     threadID: The ID of the thread to check.
Threads.isRunning = function(threadID)
{
if (threadID === 0) {
return false;
}
for (var i = 0; i < this.threads.length; ++i) {
if (this.threads[i].id == threadID) {
return true;
}
}
return false;
};

// .kill() method
// Prematurely terminates a thread.
// Arguments:
//     threadID: The ID of the thread to terminate.
Threads.kill = function(threadID)
{
for (var i = 0; i < this.threads.length; ++i) {
if (threadID == this.threads[i].id) {
this.threads.splice(i, 1);
--i;
}
}
};

// .renderAll() method
// Renders the current frame by calling all active threads' renderers.
Threads.renderAll = function()
{
Link(Link(this.threads).sort(this.threadSorter))
.where(function(thread) { return thread.renderer !== null })
.each(function(thread)
{
thread.renderer();
});
};

// .synchronize() method
// Waits for multiple threads to finish.
// Arguments:
//     threadIDs: An array of thread IDs specifying the threads to wait for.
Threads.synchronize = function(threadIDs)
{
var isFinished;
do {
this.doFrame();
isFinished = true;
for (var i = 0; i < threadIDs.length; ++i) {
isFinished = isFinished && !this.isRunning(threadIDs[i]);
}
} while (!isFinished);
};

// .updateAll() method
// Updates all active threads for the next frame.
Threads.updateAll = function()
{
var threadsEnding = [];
Link(Link(this.threads).sort(this.threadSorter))
.each(function(thread)
{
var stillRunning = true;
if (!thread.isUpdating) {
thread.isUpdating = true;
stillRunning = thread.updater();
if (thread.inputHandler !== null && stillRunning) {
thread.inputHandler();
}
thread.isUpdating = false;
}
if (!stillRunning) {
threadsEnding.push(thread.id);
}
});
for (var i = 0; i < threadsEnding.length; ++i) {
this.kill(threadsEnding[i]);
}
this.hasUpdated = true;
};

// .waitFor() method
// Waits for a thread to terminate.
// Arguments:
//     threadID: The ID of the thread to wait for.
Threads.waitFor = function(threadID)
{
var i = 0;
while (this.isRunning(threadID)) {
this.doFrame();
}
};
50
I'm having a hard time deciphering the RSS spec.  V1 is easy, it's just a header and then 64 bitmaps total (8 directions, 8 frames each).  V2 is a stumper though:

Code: [Select]
== VERSION 2 ==

There are num_direction directions stored in the file. A direction is just a row of sprites.
The first eight directions actually represent directions that the character can walk, in
the following order:

north
northeast
east
southeast
south
southwest
west
northwest

The rest of the directions can represent anything, including emotions or special movement such
as jumping or running.

---- direction header (64 bytes) ----
word num_frames;
byte reserved[62];

---- frame header (32 bytes) ----
word width; // obsolete
word height; // obsolete
word delay;
byte reserved[26];


Where in this are the bitmaps stored?  Interleaved with the headers, before them, after them...?  The spec doesn't say.  Also, only V3 allows custom-named directions; when loading V2 spritesets with more than 8 directions, what do I set as a default name for directions past the eighth?
51
Engine Development / Sphere 5.5.1 (miniSphere)
This is miniSphere, the current official implementation of Sphere and a complete rewrite of Chad Austin's original Sphere game engine, coded from the ground up in C.  It uses Allegro for cross-platform graphics and sound and JavaScript is powered by ChakraCore.  The engine boasts almost full parity with the Sphere 1.5 API and most Sphere 1.x games run in miniSphere with no modifications.  But make no mistake: this is a Sphere v2 engine, and includes many, many features not found in the engine it replaces.  These include Galileo, a scene graph API pioneered by TurboSphere which enables more flexible rendering than the Sphere v1 primitive-drawing functions; the RNG class for ultimate control over random number generation; SphereFS, a standard protocol for specifying asset filenames within the sandboxed file system; and full native support for JavaScript module syntax (import/export) as defined in ES2015.  Best of all, old and new API functions can be mixed within the same codebase, making migration of existing codebases to Sphere v2 very easy.

miniSphere natively supports many modern JavaScript features such as template strings, arrow functions, destructuring assignment, even modules!  The engine also comes with an SCons-inspired programmable build engine, Cell, also JS-powered, which can be used to build assets on-the-fly.  miniSphere was also the first implementation of Sphere to include support for single-step debugging!  Thanks to ChakraCore's powerful built-in debugger API, it does!  Using either Sphere Studio or the SSj command-line debugger (both included with the engine), you can set breakpoints, see console output, and step through your game's code line-by-line to find bugs, inspect variables and even run arbitrary JavaScript code.  It's an invaluable tool for any serious game developer.  Some might say it's miniSphere's killer feature!

Sphere 5.5.1 - September 13, 2020 - Release Notes - GitHub Repo
Includes miniSphere, SpheRun, Cell compiler, and SSj command-line debugger.
Windows release also includes Sphere Studio (.NET 4.5+ required).


note on macOS release:
The macOS release is maintained by @Rhuan and can sometimes fall a release or two behind.

Sphere v2 API Documentation
  • Sphere v2 Core API Reference - v5.5.1
  • Sphere Runtime API Reference - v5.5.1
  • Cellscript API Reference - v5.5.1
52
So after agonizing for weeks trying futilely to get my recursive threader to play along with the default map engine, I finally decided to chalk it up as a lost cause (realizing that UpdateMapEngine() doesn't check for input was a big part of this decision) and write my own map engine for Specs.  My question is, how feasible is this in Sphere 1.5?  I know TurboSphere is going to have its map engine all in script, but that's modern V8, i.e. blazing fast.  The ancient SpiderMonkey in Sphere 1.5 is slow as hell though, and I'm worried it won't be up to the task of running a full-featured map engine entirely in script...
53
Off-Topic Discussions / Visual Studio Community Edition
Has anyone seen the new Community Edition of Visual Studio?  Basically, it's Visual Studio Professional, but completely free for individual developers and small business use.  Corporate use still requires a Professional license, however.

No more pirating Visual Studio, yay!

http://www.visualstudio.com/en-us/news/vs2013-community-vs.aspx
54
This seems like a map engine bug to me, so I'm posting it in Map Support, but I'm actually not positive.

I encountered a very strange issue after implementing random battle triggering in Specs: When a battle is triggered, a bunch of stuff is skipped, despite no actual lag occurring at all.  It appears there's some erroneous frame-skipping going on here for reasons I can't fathom...

Since this is a difficult issue to explain properly, here's a demo that illustrates the problem:
https://drive.google.com/file/d/0BxPKLRqQOUSNWC03Vk9VTWZfYms/view?usp=sharing

Stand around on the map for a bit, then try to trigger a battle.  Once the battle music starts, you'll see a variable number of frames skipped equal to the number of frames spent on the field.
55
So I want to do a generic implementation of random battle triggering that doesn't require the checks to be copy/pasted into every single map script.  The thing is though, I'm not sure how to approach this.  Essentially what I want is to be able to do this once:
Code: (javascript) [Select]
RandomBattles.attach("Player");


And then in each map script:
Code: (javascript) [Select]
RandomBattles.setEnemies('lumisquirrel', 'ghostThing' /*[...]*/)

And from then on have it continue to work no matter what other maps the player moves to.
56
Programming / git integration in Visual Studio
So I've been trying out the Visual Studio 2013 preview and it's really nice.  Faster than 2012 certainly, but the thing that caught my attention was that it had git support built in out of the box.  It's nothing super fancy, but all the basic functionality is available, meaning you don't have to open the GitHub client when you just want to make a commit or view diffs, you can do it without ever leaving the IDE.

Anyway, long story short, I did some digging and found out that Microsoft has a separate download available to add git support to VS2012, so it's not exclusive to VS2013 like I initially thought.  If anyone is interested (I know some people hate GitHub's client), you can get the plugin here:
http://visualstudiogallery.msdn.microsoft.com/abafc7d6-dcaa-40f4-8a5e-d6724bdb980c
57
Script Support / Issue with updatescript
So I just found out the hard way that, if a call to the updatescript is in progress, it won't be called again.  Normally this isn't an issue, but in my particular case it's a big problem.

You see, in the Specs engine, I've implemented a custom threader to allow simultaneous operations (e.g. asynchronous tweens, or have a separate thread for battle menus while the battle engine runs in the background, that kind of thing).  If you need to wait for a certain thread to complete without affecting other asynchronous stuff, you call Threads.wait(); which enters an update/render loop until the specified thread terminates.  Here's the problem, though: MapEngine() runs its own game loop.  Which means, in order for the threader to still work on the field, I have to add calls to Threads.updateAll and Threads.renderAll to the update and render scripts respectively.  This is fine and works 95% of the time--right up until somebody calls Threads.wait().  Since that almost always happens in a thread's update function, this blocks the updatescript itself while the threader runs its wait loop--which itself includes a call to UpdateMapEngine()! This in turn causes the entire threader to deadlock.

There's a couple workarounds I've considered already. One was to forego the update/renderscripts entirely and just add a thread that updates the map engine itself. While this would keep the map engine running during battles, menus, etc, the threader would be unavailable on the field.  This is the cleanest method, but the lack of threading on the field will likely be a deal breaker.

Another method is to call UpdateMapEngine AND Threads.updateAll in the wait loop.  Unfortunately, this causes double updates when the updatescript doesn't happen to be blocked--for example, when Threads.wait is called from a persist.js map script.

Then you have the last method, the one I'm using now, which doesn't call UpdateMapEngine at all, only Threads.updateAll.  The problem here should be obvious: The map engine is frozen in place until control returns to the original MapEngine game loop (e.g. when the battle is over).

None of these solutions is really ideal for what I'm going for with Spectacles, so I'm going to ask, is there a better method to solve this problem than the three fixes above?
58
The current version is kh2Bar 2.0 released July 10, 2013. A demo project and screenshot are attached.



Changelog:
v2.0

  • New maxSectors argument for kh2Bar() constructor, useful if you need to guarantee a gauge can fit a certain number of reserve sectors.

  • New combo functionality: Call .beginCombo() to start a combo. Any damage sustained during the combo won't fade away until .endCombo() is called.

  • New .changeColor method to adjust the color of the gauge after creation.

  • Fixed long-standing glitchiness when recovering HP.

  • Gauge now drains smoothly when reading is changed.

  • Added checks for correct number of arguments to user-facing methods.


v1.6.1:

  • Updated fill logic so bar is never empty if HP is nonzero


v1.6:

  • Lots of refactoring.

  • Renamed render() to draw().

  • New show/hide functionality with optional fade in/out. Gauge is hidden upon creation.


v1.5.1:

  • Fixed a few off-by-one errors in kh2Bar.render() that were causing the gauge to be filled inconsistently depending on the amount of damage displayed.


v1.5:

  • Updated presentation: The new version is much more faithful to the KH2 design. The main lifebar is right-aligned, and if the bar on top is only a partial (as when the gauge is full and maxHP isn't an exact multiple of the sector size), the bar underneath shows through.
  • Constructor no longer takes x and y parameters; these have been moved to kh2Bar.render().

  • Changed 'reading' property to plain methods as getter/setter methods are SpiderMonkey-specific.

  • The size of the gauge is no longer hard-coded: kh2Bar.render() now also takes 'width' and 'height' as parameters.

  • Sector size (HP per bar) is now customizable via an optional constructor parameter. The default is 100.

  • Color of the gauge is also customizable. The default color is green (#00FF00 @ 100% opacity), as in KH.


v1.0:

  • First release.



Usage:
For each kh2Bar you have on-screen, you'll have to call its .update() and .draw() methods once per frame.
Code: (javascript) [Select]
RequireScript("kh2Bar.js");

// maxHP is required, sectorSize and color are optional.  If not provided, the default values are
// sectorSize: 100 (100 HP per bar) and color: #00FF00 (green) @ 100%.
var lifebar = new kh2Bar(maxHP, sectorSize, color);
lifebar.show();  // bar is initially hidden, must call show() or the player won't see it

// Inside your game loop:
lifebar.update();
lifebar.draw(x, y, width, height);



Whenever the amount of HP changes and you want the lifebar to reflect it:
Code: (javascript) [Select]
lifebar.set(currentHP);



If you need to hide the gauge temporarily (for a cutscene, etc.) just call hide(), and later show() when you need it to be visible again.  Both take an optional parameter specifying the amount of time over which to fade it in/out.
59
Projects / Spectacles: Bruce's Story
The latest release was on April 29, 2014.
Screenshots are attached; see below.

Bruce's Story is the first installment of the three-part Spectacles Saga, a series of role-playing games following the exploits of Scott Starcross as he attempts to find and defeat the Primus, an exceedingly powerful magic user who threatens both worlds. According to a prophecy made years before the saga begins, Scott, the eleventh Littermate, is the only person capable of stopping the Primus with the aid of the Spectacles, a legendary pair of eyeglasses granting their wearer magical protection--ostensibly making Scott the chosen one. But all may not be as it seems...

So the plot for this is essentially finished, along with about 50% of the dialogue.  Programming-wise, however, it's still just a battle engine, which is quite far along.

Spectacles: Bruce's Story on GitHub:
https://github.com/powercommand/spectacles-i

Current Release - 2014-04-29 - "Hollow Mind" Battle and AI demo:
https://drive.google.com/file/d/0BxPKLRqQOUSNbXBfbFprMVVOMm8/edit?usp=sharing
(after opening the above link, click File->Download to get the archive.)

Technical Details:

  • Resolution - 320x240

  • Battle System - Count Time Battle (like FFX)



Characters:

  • Scott Starcross - The protagonist of Bruce's Story, Scott is apparently the eleventh of the Littermates, a group of 11 children with a common father, so named due to their father's initial relunctance to give them names. One uncharacteristically windy night, Scott's best friend Robert Spellbinder goes missing and Scott wanders into the woods while looking for him, landing him in the dream world of Lucida, which is ruled by creatures known as nightmares.  Scott soon learns of a prophecy that names him as the one chosen to defeat the Primus--whose identity turns out to be none other than Robert Spellbinder (that was already a bit of a spoiler, so I'll avoid saying any more plot-related about Scott :)). Scott is naive to a fault, often nearly to the point of being willfully ignorant. He is also very, very forgiving, able to ignore some very large transgressions in favor of looking to the future. Scott wields a sword in battle.

  • Bruce Arsen - Once one of Scott's closest friends, but ironically one who Scott inherently distrusts. Bruce has been trying to convince Scott for a long time that Robert is untrustworthy, but to no avail. His tenacity in this has caused Scott to push him away further and further, having nearly reached the breaking point by the time Bruce's Story gets underway. Bruce appears to be narrating (hence the title), but exactly from what vantage point this narration comes remains a mystery.  In battle, Bruce wields a gun, initially a rifle passed down from his father but later on gains use of handguns.

  • maggie - No, this is not a typo, maggie's name really is spelled in lowercase. Belonging to a rare class of nightmares, hunger-pigs, born of dreamers' primal fears of being eaten, maggie is the former leader the neo-Hippos, a gang of hunger-pigs whose sole purpose for existing is to put prospective members through a rigged initiation process in which the condition for membership is to be devoured by an established Hippo.  maggie has since denounced her role in the group, and upon meeting up with Scott in Lexington Manor, joins his group.  In battle, maggie can devour her opponents (including most bosses!), often gaining the use of her quarry's skills in the process.

  • Elysia Ilapse - Daughter of Justin Ilapse, the creator the Spectacles. Elysia gives Scott the glasses shortly after meeting him, in the name of fulfilling the prophecy. Scott meets her while she is on the run from the neo-Hippos, who have been terrorizing her for years. Why they targeted her (and her father) specifically remains an open question. Elysia wields a bow and arrows in battle.

  • Robert Spellbinder - Son of Victor Spellbinder. Robert was Scott's best friend, but disappeared into the woods right before the beginning of the saga. Finding out why he disappeared so suddenly is a major driving force in the first leg of Bruce's Story (especially for Bruce), but events soon take a more sinister turn, conspiring to make Robert into the antagonist.  Hint: Refer to the spoiler in Scott's description above.

60
Scenario is a cutscene engine for Sphere which allows you to coordinate complex cutscenes and other actions (such as animations) using multiple timelines and cooperative multithreading.

The current version is Scenario 3.8.1, released on March 14, 2015.

Get the script:


Changelog:
v3.8.1
  • Added support for minisphere's IsSkippedFrame.
v3.8
  • Refactored entire codebase and added documentation comments for all methods and scenelets.
  • Fixed bugs when forking inside of a doWhile loop where instructions inside the loop could overwrite each others' state.
  • Changed doWhile and doIf to take a function (lambda) as argument. The function will be called at scene time and should return true or false to determine how to proceed.
  • Removed scenelets doUntil and set.
v3.7.3
  • Hotfix for broken doIf and doUntil scenelets.
v3.7.2
  • Hotfix release to fix call scenelet regression in 3.7.
v3.7.1
  • Hotfix release to restore fork functionality which was completely broken in 3.7.
v3.7:
  • BREAKING CHANGE - Removed 'state' argument from all scenelet functions; scenelets should store state data using 'this' instead. Any custom scenelets will have to be rewritten.
  • New optional finish handler for scenelets, called immediately after the scenelet stops running (i.e. when its update function returns false).
  • Support for variables: Use set to set a variable; increment or decement to add or subtract 1 from its value, respectively. Variables can be accessed in scenelet code via scene.variables['var_name']
  • New control-flow commands: doIf, doWhile, doUntildoIf executes a block of commands only if a specified condition is met, the other two are similar but execute the same block repeatedly (loop) while or until the condition is met, respectively.
v3.6:
  • Renamed beginFork and endFork to fork and end, respectively.
  • New scene looping feature: Pass 'true' to the Scenario() constructor for an endlessly looping scene. This is useful for running looping animations in the background. Call Scenario.stop() if you need to break the loop.
  • New optional waitUntilDone argument for run() that mimics the old blocking behavior. Helpful for cutscenes.
  • Color mask for the fadeTo command is now shared by all scenes and persists after a scene ends. Don't forget to fade back in or the player won't be able to see anything!
  • Screen fades and camera manipulations are no longer reverted automatically at the end of a scene. This must now be done manually.
  • Running a scene no longer clears its command queue.  This lets you run the same scene more than once. Great for caching animation commands!
v3.5.4:
  • Sphere map engine is no longer required to be running to execute a scenario.
v3.5.3:
  • Running a scenario no longer detaches input automatically. If this is needed, you must detach and reattach the input yourself.
v3.5.2:
  • Added new commands marquee and tween. The latter is great for coordinating complex UI animations.
v3.5.1:
  • Removed unusable marquee command.
v3.5:
  • User is required to call Scenario.initialize before using the engine.
  • Calls to Scenario.update and Scenario.render must be added to the update and render scripts, respectively.
  • run() now returns immediately instead of running its own update loop. If your game relied on the old blocking behavior, you'll have to modify it.
  • New method: scene.isRunning checks whether the scene is still executing and returns true if it is, false if not.
v3.1:
  • Built-in commands now use seconds instead of milliseconds for duration.
  • Renamed 1 identifier:
    • checkInput -> getInput
  • Added support for cascaded command calls. This is the preferred method for scene composition now; examples updated accordingly.
  • Completed refactoring started in 3.0.
v3.0:
  • Major refactoring to make the codebase more readable and less bug-prone.
  • Renamed 4 identifiers:
    • defineAction -> defineCommand
    • walkPerson -> movePerson
    • execute -> run
    • handleInput -> checkInput

How to use Scenario:
First things first: The Sphere map engine must be running in order for Scenario to work. Then when you need a cutscene, you write something like this:
Code: (javascript) [Select]
DetachInput();
new Scenario()
    .marquee("Chapter 13: Some Random Girl Blows Up")
    .movePerson("Katelyn", "north", 100, 2, true)
    .pause(2.0)  // 2-second delay
    .killPerson("Katelyn")
    .playSound("BigExplosion.wav")
    .run(true);  // true to wait until scene finished, false to run in background
AttachInput("hero");

The real power of Scenario, however, lies in its forking feature, which enables multiple simultaneous timelines. For example, you can have the background music (or the screen!) fade in while other actions take place at the same time. As in this example:
Code: (javascript) [Select]
DetachInput();
new Scenario()
    .fork()  // fork the timeline
        // we're fading in, so we specify a transparent color (alpha=0) to fade to
        .fadeTo(CreateColor(0, 0, 0, 0), 5.0)  // fade in over 5 seconds
    .end()  // end the fork block
    .marquee("Chapter 13: Some Random Girl Blows Up")
    .movePerson("Katelyn", "north", 100, 2, true)
    .synchronize()  // pause until all forks are finished - we want to make sure the fade-in has finished before she blows up!
    .killPerson("Katelyn")
    .playSound("BigExplosion.wav")
    .run(true);
AttachInput("hero");

Starting with version 3.7, Scenario also includes support for conditional execution and looping.
Code: (javascript) [Select]
// The following scene flashes the screen to white and back twice:
var flashes = 0;
new Scenario()
    .doWhile(function() { return flashes++ < 2; })  // 2 iterations
        .fadeTo(CreateColor(255, 255, 255, 255), 0.25)
        .fadeTo(CreateColor(0, 0, 0, 0), 0.25)
    .end()
    .run();