This guide is intended as a reference for scripters who are implementing new features for the game. Before scripting anything be sure to let me know you are scripting and what features you are going to attempt to implement. This way I can ensure 2 people are not working on the same feature and also make sure you have access to the variables you need for your scripts. You can contact me at support@BlindAudioGames.com.
Since any features you add will be available to all map creators it is important to document them. There is a section in the user guide where I will link to your documentation. I use MarkDown to create the user guide and you may find this a helpful way to create html documentation. You can provide your documentation as either text or html files.
Whenever you receive a new copy of the game you should create a file in the Data for TB folder called game.config. Put one line in this file that says "scripter". This does 2 main things:
The scripting language is Javascript. I chose this language because almost every professional programmer has been exposed to it at some point or other. It is also a simple scripting language that uses dynamic types. I use the Noesis Javascript.net library to host the javascript context. And that library in turn uses the Google V8 engine which is the fastest Javascript engine available.
You should add a folder named after you in the Data for TB\Scripts folder. Then put your scripts inside that folder to keep them separate from other scripters' files. The file extension you put on your scripts does not matter. The game will parse all files inside the scripts folder as javascript. You will need to set the load_scripts= flag in your map pack settings.txt file. It takes the name of the folder you named after yourself. For instance I use:
I personally use Notepad++ for editing javascript. I know it treats the files slightly differently if you name them with a .js extension. It then knows they are javascript files and can attempt to help more. For instance by having built in hot keys to comment or uncomment a block of code using javascript like comments. This didn't work great for me but I'd love to hear if anyone else gets some of these niceties working.
A very common javascript mistake is to not declare your variables using the var keyword. When you forget to use this keyword then variables are declared in the global scope rather than the functions scope and you can get conflicts between variables of the same name that are used in different functions.
Also be aware that all scripters share the same environment so your scripts may conflict with others if you have declared the same function names.
Another common mistake in this javascript hosting environment is to get the letter casing incorrect when accessing variables. The naming convention for C# (which is what the engine is written in) uses title casing. If you're getting an error and things look right then you should double check that all the capitalization is correct.
Here are the functions that are globally available:
Because of the limitations of my current javascript library I can not serialize and deserialize the entire javascript context when saving and loading games. So I have defined a special variable called "global" that I am able to save and load. This object gets initialized to an empty object whenever you start a new map. If you need to store counters or other data that needs to be remembered beyond the local scope of your function this is the place to do it. I recommend setting a property with your name on the global object. Then you can have a local variable g that points to your global property. This way your variables will not conflict with anyone elses. Here's an example. Place this line in the global scope of your script.
global.ian_reed = {};
Then place this line at the top of each of your functions that need to alter your global object's properties.
var g = global.ian_reed;
And now you can reference the g variable in your functions and set and get properties from them like so.
g.poison_counter += 1; say(g.poison_counter);
There are 2 ways to get at objects and functions that exist within the core game engine. One of these ways is through variables that are passed into your function. The other is through the "shared" object. The variables passed in to your function are related to the event that you are responding to. The shared object holds objects that are available at all times such as lists of all units or all tiles in the current map. Here's an example of using the shared object:
var tile = shared.Map.ReviewTile;
See the object hierarchy section near the end of this guide to find out what properties and functions are currently exposed on the shared object and descendant objects.
You can define a special file called Event Subscriptions.txt in your scripts folder. See the example in the Data for TB\Scripts\Ian Reed\Non Script Files\Event Subscriptions.txt file. Each event line has the name of the event followed by the function that should be called for that event, followed by an optional JSON object notation to define arguments that will be passed to the function.
Valid event names are:
I have some example functions that you might subscribe to each of these events. Each event passes arguments that may be needed for that event and in the examples they are assigned to local variables with comments. Override events return a JSON object with a handled boolean set to true or false to indicate whether this function decided to handle the event. If true the engine will not run any other javascript or C# code setup to handle this event. In this way you can truly override the logic and stop the engine from performing the built in logic. I've prefixed the function names with my initials "ir_" and recommend you prefix them with your initials so we don't have naming conflicts between scripters.
function ir_on_map_key(args) { var key = args.k; var mod = args.m; // You can compare the integer values stored in key and mod to the values defined in Built in\\Key Definitions.txt. } function ir_after_map_load(args) { var map = args.map; } function ir_after_create_unit(args) { var unit = args.unit; } function ir_after_unit_death(args) { var unit = args.unit; } function ir_after_perform_skill(args) { var source = args.source_unit; var target = args.target_unit; var targetTile = args.target_tile; var skill = args.skill; var success = args.success; // indicates whether the skill was successful after the chance roll. } function ir_after_use_item(args) { var source = args.source_unit; var target = args.target_unit; var targetTile = args.target_tile; var item = args.item; var success = args.success; // indicates whether the skill was successful after the chance roll. } function ir_after_effect_applied(args) { var source = args.source_unit; var target = args.target_unit; var targetTile = args.target_tile; var effect = args.effect; } function ir_after_effect_removed(args) { var source = args.source_unit; var target = args.target_unit; var targetTile = args.target_tile; var effect = args.effect; } function ir_after_effect_fizzled(args) { var target = args.target_unit; var tile = args.target_tile; var effect = args.effect; } function ir_after_turn_ended(args) { var units = args.ending_units; // a list of units on the team that is ending their turn. } function ir_after_turn_started(args) { var units = args.starting_units; // a list of units on the team that is starting their turn. } function ir_override_handle_causers(args) { // this function is responsible for dealing all damage and healing for any skill, item, or effect var effecter = args.effecter; // the skill, item, or effect causing damage or healing. var skill = args.skill; // the effecter, if it is a skill var item = args.item; // the effecter, if it is an item var effect = args.effect; // the effecter, if it is an effect var unit = args.cur_unit; // the unit using the skill or item, if any var target = args.target_unit; // the target of the skill, item, or effect var targetTile = args.target_tile; // the tile being effected, if in TB combat return { handled: true }; // handled indicates this script decided to handle it and the engine should not do any further perform skill logic. } function ir_override_tb_team_ai(args) { var units = args.units; // the list of units that need their AI run. var friends = args.friends; var enemies = args.enemies; return { handled: true }; // handled indicates this script decided to handle it and the engine should not do any further AI logic for this team. } function ir_override_tb_unit_ai(args) { // This method will get called multiple times for each unit until all units report that they did not do anything. // If the override_tb_team_ai event is handled then this function will never get called as the team level AI supersedes this logic. var unit = args.unit; // the single unit that needs it's AI run. var friends = args.friends; var enemies = args.enemies; return { handled: true, did_something: true }; // handled indicates this script decided to handle it and the engine should not do any further AI logic for this unit. // did_something indicates whether this unit performed an action or not. } function cb_after_item_equipped(e) { var unit = e.unit; // the unit that equipped the item var item = e.item; // the item equipped } function cb_after_item_unequipped(e) { var unit = e.unit; // the unit that unequipped the item var item = e.item; // the item unequipped } function cb_before_point_damage(e) { var source = e.source_unit; // the unit doing the damage var target = e.target_unit; // the unit being damaged var skillOrItem = e.effecter; // the skill or item doing the damage var point = e.point; // the point (as a point object) effected var amount = e.amount; // the damage that will be done return { handled: false }; // handled indicates this script decided to handle it and the engine should not do any further point damaging logic } function cb_after_point_damage(e) { var source = e.source_unit; var target = e.target_unit; var skillOrItem = e.effecter; var point = e.point; var amount = e.amount; } function cb_after_unit_move(e) { var unit = e.unit; // the unit that moved var added = e.unitAdded; // true if this unit was added to the map, false if it was just a movement var origin = e.origin; // the tile that this unit previously occupied. If added is true, this will be null } function cb_is_skill_or_item_usable_before_target(e) { var source_unit = e.source_unit; // the unit attempting to use this skill or item var effector = e.effector; // the skill or item trying to be used var suppress_speech = e.suppress_speech; // true on AI checking if something is useable turns, false otherwise. It's recommended you check this before saying anything in this function return { useable: true }; // return useable true to allow this skill or item to be used. false prevents its use } function cb_is_skill_or_item_usable_with_target(e) { var source_unit = e.source_unit; // the unit attempting to use this skill or item var target_unit = e.target_unit; // the target of this skill or item. Will be null when the target is a tile var target_tile = e.target_tile; // the target tile for this skill. Will have a value even when the taret is a unit var effector = e.effector; // the skill or item trying to be used var suppress_speech = e.suppress_speech; // true on AI checking if something is useable turns, false otherwise. It's recommended you check this before saying anything in this function return { useable: true }; // return useable true to allow this skill or item to be used. false prevents its use }
Script flags are a way for you to let map creators utilize your scripts in the same way that they use flags to utilize core engine features. You can define a special file called Script Flag Definitions.txt in your scripts folder. See the example in the Data for TB\Scripts\Ian Reed\Non Script Files\Script Flag Definitions.txt file.
There are a couple comments in the file that I'll repeat here. // each line has the flag name followed by the type. Valid types are string, bool, int, float, percent, keys, function, and enum followed by a pipe and list of enums separated by commas. // You can optionally have a list of file types that the flag can be used in, separated by commas. If left off the flag is valid in all files.
Down the road the valid files might not be optional, I recommend specifying them as it will give map creators a friendly error when they accidentally use a script flag in the wrong file. Let me comment on each type:
Each object that supports script flags has a new dictionary on it called ScriptFlags. Here's an example of me getting the announce_name script flag off of a unit in my testing.js file included with the game:
function announce_message(args) { if (args.unit) { if (args.unit.ScriptFlags.get("announce_name")) { say(args.unit.FriendlyName); } } say(args.message); }
So again that was
var val = unit.ScriptFlags.get("announce_name");
If you try to get a script flag that was not defined in an object's file then null will be returned.
In general you will subscribe one function to each global event in the event subscriptions.txt file. You should base the behavior of your scripts on script flags that map creators have defined on their files. If they have not set any script flags your scripts probably shouldn't do anything. This keeps control in the map creators hands so they can choose to use a feature you provide through script flags without getting an unwanted feature that occurs when your scripts are loaded regardless of what script flags are set.
Each of the below sections lists the type of object in the heading tag followed by each property and function exposed on it in a bulleted list. Note that Units and Structures are actually both the Unit type under the hood but that Structures have a boolean set to true so I can treat them differently in the few cases they need to work differently.
Most objects now have both an IndexedName and a FriendlyName. The IndexedName is used for hash indexing and for comparison to see if 2 objects had the same file name and will be unique across objects created from different files. The FriendlyName includes capitalization and is the form presented to players both visually and through speech. The FriendlyName can be set to the same value between 2 files. For instance you might have a floor terrain and a trapped floor terrain but set both of them to have the same friendly name of "Floor" so that players can not tell which tiles are trapped and which ones are not. So the rule of thumb is to use IndexedName for comparisons and hash indexes, but FriendlyName for text to speech and visual text. The Unit object has IndexedType and FriendlyType proeprties instead because it's Name property is used for naming units and was never based on the file name.
When you get a List or Dictionary (hash for short) from the engine it will not work like a normal javascript array. In fact any object obtained from the shared object will not work like a normal javascript object. Specifically these objects can not have their properties enumerated by for looping over them as is normal in javascript. Here's an example of what you can not do:
var tiles = shared.Map.AllTiles; for (var key in tiles) { say("tile " + shared.GetPos(tiles[key])); }
Instead when working with lists you must get their length and use a for loop that increments an indexer, such as the following:
var tiles = shared.Map.AllTiles; for (var i = 0; i < tiles.length; ++i) { say("tile " + shared.GetPos(tiles[i])); }
Here's an example of announcing all the keys and values of a dictionary.
var g = shared.GetStorage("ian_reed"); var keys = g.keys; for (var i=0; i < keys.length; i++) { var key = keys[i]; var value = g.get(key); say(key + " = " + value); }
after_removed
event will trigger and the removal will be read aloud if the effect has the announce flag.The old events are similar to flags and are defined in the same area as flags. I'm trying to move away from these old events and toward the new event subscription and script flags because I think they make a better experience for map creators. The old events must be on their own line unlike flags and you can only have one event per line. The name of the event and the object it is added to define when the event will occur. Each event can have multiple functions subscribed to it. You do this by defining each new event subscription on a new line.
Here's an example of an event that can be placed in the default map flags file:
on_key=set_point@ { key: x_key, mod: shift_mod, pointname: "health", value: "5r15" }
on_key is the name of the event. set_point is the name of the function that will be called when the event occurs. Everything after the @ sign is JSON (Javascript object notation). The JSON defines the initial properties for the object that will be passed to your function. This is the perfect place for map creators to specify input to your function. This JSON object will have game engine defined properties added to it based on which event is called.
In my functions I call the parameter args but you can give it any name you like.
The on_key event allows you to hook into any key press the user makes. I recommend letting the map creator specify which key combination they want in the JSON parameters. You can see some examples of on_key events in use in my testing.js file. You will also find the key_definitions.txt file helpful for using friendly names that correspond with the key numbers.
The on_key event gets these properties passed by the engine into it's local object:
This event is defined on skills and occurs after that skill has been used. This gives you a place where you can make additional changes to the source or target unit after the engine has done all the things it supports.
The after_perform event gets these local properties:
This event is defined on effects and occurs after that effect has been applied to a unit or tile.
The event gets these local properties:
This event is defined on effects and occurs after that effect has been removed through the use of a skill or item. This does not occur when the effect fizzles because it's time duration expires.
The event gets these local properties:
This event is defined on effects and occurs after that effect has fizzled due to it's duration expiring. This does not occur when the effect is removed by a skill or item.
The event gets these local properties:
This event is defined on units and occurs after that unit dies.
The event gets these local properties:
This event is defined on units and occurs after the unit is created.
The event gets these local properties:
First off I want to say thanks for being willing to contribute your time to create new features for the game. It helps me and everyone else who plays or creates maps for Tactical Battle.
There are obviously many object properties that are not yet exposed. As has been discussed this is because it makes the most sense for me to only expose things you actually need for your features. This allows me to make more changes with a lower possibility of breaking your scripts. It also allows me to cater the things I expose to make the most sense for how you are going to utilize them. If you need access to something just let me know what you need it for and I'll see what I can do about getting it exposed.
Please realize that I may make your scripts obsolete at times. I don't mean to hurt your feelings by doing this. It will generally only be done if I feel I can give a better user experience because I'm able to access all the insides of the engine.
It has been a lot of work to add support for scripting but I think it will be a great addition to the game and I appreciate your help to make it a worthwhile feature. Good luck!
Written by Ian Reed