The goal of TBAF is to provide saner IE scripting experience. To that end, TBAF supports a limited selection of TS language features like loops, nested conditions, functions. Although that's not much, it's still considerably better than BAF's only IF-THEN clause.
Of course, there's already SSL which has more or less the same goal. However, Typescript is much more mainstream, and I think, easier to get started with. And it allows to take advantage of the far superior Typescript tooling.
At this moment, it's released as feature preview, so could be a little rough around the edges, and syntax may evolve. Make sure to check the generated BAF.
General
TBAF is Typescript. If you're familiar with Typescript and BAF, then you will have no problem writing TBAF. If you're not familiar with Typescript, do not worry. TS is extremely popular, and TBAF only uses a very limited list of TS features, so it's quite easy to understand. To get a feeling of how TBAF looks and why'd you want to use it, take a look at the following example
Code: Select all
/** I am doomed! */
const LVAR_doomed = "doomed"
/** Fires a spell-trigger just to show what a great mage he is */
const LVAR_castSpellTrigger = "SpellTrigger"
// First talk, then fight
if (See(Player1) && Global(LVAR_doomed, LOCALS, 0)) {
SetGlobal(LVAR_doomed, LOCALS, 1)
FaceObject(Player1)
SmallWait(8)
StartDialog("WM_RHIA", Player1)
}
/** Start fight: spell trigger */
/** Common spell trigger actions */
function actSpellTrigger() {
DisplayString(Myself, 39968) // ~Spell Trigger - Fired~
ReallyForceSpellRES("WM_LUCK", Myself)
SetGlobal(LVAR_castSpellTrigger, LOCALS, 1)
}
if (See(NearestEnemyOf(Myself)) && Global(LVAR_castSpellTrigger, LOCALS, 0)) {
if (LevelLT(LastSeenBy(Myself), 3)) {
actSpellTrigger()
ReallyForceSpell(Myself, WIZARD_ARMOR)
ReallyForceSpell(Myself, WIZARD_SHIELD)
}
if (LevelLT(LastSeenBy(Myself), 8)) {
actSpellTrigger()
ReallyForceSpellRES("WM_LIGHT", Myself)
ReallyForceSpell(Myself, WIZARD_SHIELD)
}
}
/** End start fight: spell trigger */
Hopefully this self-explaining enough to see both how and why.
In order to code in TBAF you will need to install IElib npm package, which provides BAF definitions for intellisense. Refer to the example repository to see how to do that. Note that at this moment IElib-ts only supports classic BG2 scripting style, EE additions are not included. And even BG2 support is WIP - this is where you come in.
Features
There's a number of caveats to all of these, so make sure to read to the end to understand the limitations.
Variables
Variables are a basic requirement for more advanced language features. Typescript is a strongly typed language, so you will have to declare every variable (and sometimes its type too).
Code: Select all
const LVAR_castFireball = "fireball";
if (See(TenthNearestEnemyOf(Myself)) && Global(LVAR_castFireball, LOCALS, 0)) {
IncrementGlobal(LVAR_castFireball, LOCALS, 1)
ReallyForceSpell(LastSeenBy(Myself), WIZARD_FIREBALL)
}
is equivalent to
Code: Select all
if (See(TenthNearestEnemyOf(Myself)) && Global("fireball", LOCALS, 0)) {
IncrementGlobal("fireball", LOCALS, 1)
ReallyForceSpell(LastSeenBy(Myself), WIZARD_FIREBALL)
}
Which is transpiled into the following BAF
Code: Select all
IF
See(TenthNearestEnemyOf(Myself))
Global("fireball", "LOCALS", 0)
THEN
RESPONSE #100
IncrementGlobal("fireball", "LOCALS", 1)
ReallyForceSpell(LastSeenBy(Myself), WIZARD_FIREBALL)
END
Hopefully, the conversion is obvious enough, so in the following examples BAF will be omitted.
Important: there is no runtime. You cannot change variables. Therefore, TBAF variables are always constant and declared with
const
(with one exception, see below).Loops
TBAF supports simple
for
and for..of
loops (examples are contrived, just for illustration):Code: Select all
for (let rng = 0; rng < 2; rng++) {
if (See(NearestEnemyOf(Myself)) && Range(LastSeenBy(Myself), rng)) {
ReallyForceSpell(LastSeenBy(Myself), WIZARD_LARLOCH_MINOR_DRAIN)
}
}
const players = [Player1, Player2]
for (const player of players) {
if (See(TenthNearestEnemyOf(player))) {
ReallyForceSpell(LastSeenBy(player), WIZARD_LARLOCH_MINOR_DRAIN)
}
}
turns into
Code: Select all
IF
See(NearestEnemyOf(Myself))
Range(LastSeenBy(Myself), 0)
THEN
RESPONSE #100
ReallyForceSpell(LastSeenBy(Myself), WIZARD_LARLOCH_MINOR_DRAIN)
END
IF
See(NearestEnemyOf(Myself))
Range(LastSeenBy(Myself), 1)
THEN
RESPONSE #100
ReallyForceSpell(LastSeenBy(Myself), WIZARD_LARLOCH_MINOR_DRAIN)
END
IF
See(TenthNearestEnemyOf(Player1))
THEN
RESPONSE #100
ReallyForceSpell(LastSeenBy(Player1), WIZARD_LARLOCH_MINOR_DRAIN)
END
IF
See(TenthNearestEnemyOf(Player2))
THEN
RESPONSE #100
ReallyForceSpell(LastSeenBy(Player2), WIZARD_LARLOCH_MINOR_DRAIN)
END
Important:
-
for
loop is the only place where you will use let
instead of const
in TBAF.- Only simple iterators and conditions are supported, like in the example.
Nested conditions and Else clause
Nested conditions are flattened.
Else
statements are inverted into if
's and also flattened.Code: Select all
if (Global(LVAR_contingency, LOCALS, 0) && TookDamage()) {
// Level 1-14
if (LevelLT(LastSeenBy(Myself), 15)) {
DisplayString(Myself, 40252); // ~Contingency - Protection from Magical Weapons~
ApplySpell(Myself, WIZARD_PROTECTION_FROM_MAGIC_WEAPONS)
SetGlobal(LVAR_contingency, LOCALS, 1)
} else {
// Level 15+
DisplayString(Myself, 43050); // Chain Contingency - Improved Mantle
ApplySpell(Myself, WIZARD_IMPROVED_MANTLE)
SetGlobal(LVAR_contingency, LOCALS, 1)
}
}
Is equivalent to
Code: Select all
if (Global(LVAR_contingency, LOCALS, 0) && TookDamage() && LevelLT(LastSeenBy(Myself), 15)) {
DisplayString(Myself, 40252)
ApplySpell(Myself, WIZARD_PROTECTION_FROM_MAGIC_WEAPONS)
SetGlobal(LVAR_contingency, LOCALS, 1)
}
if (Global(LVAR_contingency, LOCALS, 0) && TookDamage() && !LevelLT(LastSeenBy(Myself), 15)) {
DisplayString(Myself, 43050)
ApplySpell(Myself, WIZARD_IMPROVED_MANTLE)
SetGlobal(LVAR_contingency, LOCALS, 1)
}
Important: while conditions are merged, action blocks never are. Each action block is final as defined. (But you can use functions to combine multiple actions - see the corresponding section).
Functions
Or, "functions". Functions in TBAF are not real TS functions (again, there is no runtime). They actually are function-like macros. Meaning, every function call is replaced by function body, substituting variables, if any.
There are 3 kinds of functions allowed in TBAF:
- block - an entire if-then block or a list of blocks
- action - a list of actions
- trigger - a list of triggers
Block and action functions may not have a return value (they are of type
void
). Do not use return
statement in these kinds of functions.Code: Select all
/**
* If random roll is less than rollLimit, create a random magic effect after party has rested.
* Make sure to use this in incrementing order of `rollLimit`, to avoid missing effects.
* @param rollLimit roll limit
* @param headString passed to DisplayStringHead
* @param spellRes spell resource
*/
function randomEffect(rollLimit: number, headString: number, spellRes: SplRef) {
if (PartyRested() && RandomNumLT(SLEEP_MAX_ROLL, rollLimit)) {
PlaySound("EFF_M10")
DisplayString(Myself, $tra(10201)) // "Wild Magic made your dreams come true"
DisplayStringHead(Myself, headString)
ApplySpellRES(spellRes, Myself)
}
}
Trigger functions may only contain a return statement, which must be a valid combination of BAF triggers. They always return
boolean
.Code: Select all
/**
* @returns true if current creature has any of the listed wizard spells
*/
function haveWizardSpells(): boolean {
return HaveSpellRES("WM_ATTR") || HaveSpellRES("WM_RND");
}
Functions can call other functions, naturally.
Includes
Includes are normal Typescripts includes.
Code: Select all
/* lib.ts */
export function randomEffect(rollLimit: number, headString: number, spellRes: SplRef) {
// body
}
/* script.tbaf */
import { randomEffect } from "./lib" // if lib.ts is in the same directory
// "Pretty Sparkles" -> Wild Magic 30: Pretty Sparkles
randomEffect(15, 397, "SPWM130")
Handy, as you can see. Can share functions, constants, etc between multiple files.
Important: do keep
.ts
extension for libraries so that TS language server can find them.Libraries can include other libraries, naturally.
Types
As mentioned earlier, Typescript is a strongly typed language, which means every variable has a type. Most of the time, TS can infer it, but sometimes it has to be declared explicitly. For someone unfamiliar with strongly typed concept, this may seem like an unnecessary hurdle. But rest assured, as you write more complex scripts, you will come to appreciate the type system. In the end, it helps to produce much more robust code.
TBAF transpiler itself doesn't know anything about BAF types. These definitions are provided by IElib-ts.
IElib provides a degree of type safety, although it's not super strict. That's due to various factors - Typescript type system limitations, suboptimal IESDP data presentation, and the fact that learning curve with full type safety would probably too steep for an average IE modder unfamiliar with Typescript. This is subject to change, though, and in general type restrictions will likely tighten as IElib matures.
Generally, most functions will accept strings and numbers as arguments, where intuitively applicable. However, it's recommended to define or declare more specific types:
Code: Select all
function randomEffect(rollLimit: number, headString: number, spellRes: SplRef) {
// function body
}
// "SplRef" means ResRef of SPL type. It's more cosmetic for now, though.
// Recommended usage
const myspell: SplRef = "spwi123";
randomEffect(0, 0, myspell);
// But this will work too
const myspell = "spwi123";
randomEffect(0, 0, myspell);
// And this
const myspell: ResRef = "spwi123";
randomEffect(0, 0, myspell);
// And this, of course
randomEffect(0, 0, "spwi123");
// This, however, will throw a type mismatch error
randomEffect(0, 0, 123);
IElib declares many built-in IDS symbols, but that's a work in progress, and depends on user demand and help. For mod-added symbols, you can define or declare them:
Code: Select all
const MYSPELL = 2225;
HaveSpell(MYSPELL);
// This will be transpiled into "HaveSpell(2225)"
declare const MYSPEL: number;
HaveSpell(MYSPELL);
// This will remain verbatim: "HaveSpell(MYSPELL)"
In this example, if you've added your spell symbols to spell.ids, you will likely want to use the second form. Otherwise, the first one.
Notable peculiarities
$obj
function$obj
is a special function provided by IElib. It takes an object specifier string and return an object specifier type, which is subtype of ObjectPtr
- game object type.Use it like this
See($obj("[GOODCUTOFF.0.0.0.0.SUMMONED]"))
. Also can be used with death variables: $obj("Melicamp")
.(Why
ObjectPtr
and not just Object
? "Object" is a reserved word in Typescript. "ObjectPtr" stands for "object pointer").Only needed for object specifiers and death vars, not for objects.
See(Myself)
is just fine.$tra
functionSimilarly,
$tra
is syntax sugar for tra references. Where in BAF you would say @123
, in TBAF you say $tra(123)
. For example, DisplayString(Myself, $tra(30121))
.Because TS doesn't like "@".
Only one RESPONSE
Multiple
RESPONSE
blocks for the same IF
condition are not supported. Use RandomNum
instead. It works better, anyway.Scope
Functions and variables have global scope. (Or, at least, you can assume that they do). So, diversify the names, no shadowing.
At its core, TBAF transpiler is a simple text processor. Keep that in mind while naming stuff.