|
On the fourth Monday of each month, we explore the code underneath The Broken Hourglass, the game environment called "WeiNGINE." This month, we explain how scripting can cause game events to be "skipped," which is more useful than it may seem, and which, of course, the documentation discourages... Scripting in WeiNGINE keys off game events. There are roughly 90 events the scripting language can hook into, ranging from loading an area to resolving an attack. Most of the time, scripters simply want to add a special occurrence on top of the event which is already taking place, such as:
- We just entered the Wasteland district, so we want to spawn an encounter with scavengers. - We just scored a hit with a special weapon, so we want to add some bonus damage or a special effect. - Horace just spotted the PC, so we want him to approach and initiate dialogue.
But what do you do if an event is about to take place-but you want to stop it from occurring? What if the party is trying to rest somewhere resting is not allowed? What if we want to block someone from opening a door? What if Blue Valkyrie is about to die, but we want to delay that death?
That's where the "skip" concept comes in. Some of the hookable events in the game occur just before the event would happen, and the scripter may tell the game to skip that event, and optionally to take different action instead. This might include displaying a string telling the player why the event was skipped ("You can't open the door until you put on the funny hat") or taking some different action instead (such as thwarting the party's use of an area transition and teleporting them somewhere they didn't intend to go).
The usual approach to a "skip" event is that a special global variable is set to 1. These variables are only checked during the event in question and are always set back to 0 as soon as your hook completes-that is, skipping a door or areatrans or death once will not permanently condemn the event to be skipped for all time. The variables typically take the form _global.int.skip_this_foo, where "foo" is the type of event being skipped.
Consider an in-game example, the Second Wind trait. Once per day, the bearer of the Second Wind trait will "skip" dying after taking a mortal wound, and instead be restored to one-third their normal health total.
We accomplish this with the before_death event. We present for your consideration the before_death event documentation.
before_death breakdown : breakdown -> source : thing -> target : thing -> breakdown
Hooks on target ("CREATURE") and its party. Occurs after @before_damage_dealt_to when the engine has calculated that this bit of damage, plus previously-taken damage, will exceed the target's maximum health points. Set _global.int.skip_this_death to 1 if you want target to not die (and instead have 0 health points). Do not do that, however -- the typical goals for wanting to stop a death (e.g., to talk to the party before expiring) can be obtained by starting a cutscene inside your hook. You should return breakdown unchanged. See @death. skip_this_death is automatically reset to zero after this event. (Yes, that's right, we're about to do something the documentation expressly tells us not to do! These things happen.)
There are two script hooks associated with Second Wind. One is a simple @after_rest event which gives all possessors of the Second Wind trait a flag indicating that they are eligible to perform one Second Wind (cre.int.can_second_wind <- 1) The other-the one we care about, because it uses skip-is a before_death event. It is designed to prevent the death from happening, nullify the blow which would have been received, and heal the trait bearer.
Here's the code.
@before_death "second-wind" fun damage source target { let zerodam = _breakdown_add damage ((-1) * _breakdown_total damage) ("second-wind"::"TRAIT") in // All this does is cancel out the damage to zero, and credits the trait with the negative portion if (target.int.can_second_wind) then { // We check here that the target hasn't taken his or her daily Second Wind yet _global.int.skip_this_death <- 1; // We tell the engine that we definitely don't want the victim to die target.int.can_second_wind --; // We decrement the can_second_wind variable, which won't be set back to 1 until a rest _heal target (target.Health/3); // We heal the target _display_string target ~Second Wind!~; // We point out that Second Wind has been taken zerodam // We have to return a breakdown at the end of @before_death-some sort of damage still has to be done, but we'll apply our special 0-damage breakdown here } else damage // if we are not performing Second Wind, then we should take the full damage as normal fi; } foreach [ "second-wind"::"TRAIT" ] // we associate this script hook with the Second Wind trait.
Again, setting skip_this_death to 1 only causes this particular killing to be skipped. As soon as the attack ends, the second wind has been taken, etc., the game will go back to considering each potential death normally.
Most events do not have "skip" capabilities associated with them, but a skilled scripter can intercept almost any common game action at some level and insert a custom result, if desired. Generally, however, skip is only used as garnish, for special events like a trick door, or for occasional rule-bending, as in the case of Second Wind.
|