BSL:Tutorial/Scratch: Difference between revisions

From OniGalore
Jump to navigation Jump to search
No edit summary
(added info on usage of restore_game)
 
(36 intermediate revisions by 8 users not shown)
Line 1: Line 1:
This page is a short tutorial on how to get started on simple scripts for those who have had no experience with scripting before.
This page is a short tutorial for those who have no experience with scripting on how to get started creating a new level script. It's recommended that you read the [[BSL:Introduction]] page first to get a grasp of BSL's syntax. Each significant command in this tutorial should have a link behind it the first time it's referenced; click that link to learn more about the command. For a complete list of commands, see [[BSL:Functions]] and [[BSL:Variables]].
It will teach you, step by step, how to write a simple script. By the end of this tutorial, you will be able to move on to more advanced scripts.
{{TOClimit|2}}
==The "main" function==
Open up your [[IGMD]] folder and look for '''EnvWarehouse/'''. Inside this folder you should see seven .bsl files. Using a plain-text editor (such as Notepad or Notepad++ in Windows, and TextEdit or BBEdit in macOS), open the file '''warehouse_main.bsl'''. It should look like this:
#
# warehouse_main.bsl
#
var int my_save_point;
func void main(void)
{
    env_show 2010 0
    gl_fog_blue=.15
    gl_fog_red=.15
    gl_fog_green=.15
    gl_fog_start=.99
    gs_farclipplane_set 5000
    obj_create 20 20
    level_start
}


This is an example of a level's '''main''' script. It contains the main() function, which is always where the level starts. The script file can technically be called anything, but there must be a function called "main" in one of the level's script files. Oni will always look for and start with this function when a level loads.


----
==Creating a script==
Close warehouse_main.bsl if you still have it open, and rename the containing folder "EnvWarehouse-original". Create a new folder called "EnvWarehouse" and create a new plain-text file inside it called "tutorial.bsl".


== Part 1 : Knowing your formats ==
First we're just going to place you in the final room of TCTF Training where you practice firing weapons. The room will be empty since no enemies have been spawned by the script. In case you're wondering how you could be in TCTF Training when this is a script inside EnvWarehouse/, be aware that {{C0}} is actually part of the same level data files as {{C1}} and only appears to be separate in the Load Game menu (this is the only time Oni combines two levels into one set of files).


The game will accept 2 types of syntax for scripts, and they are based on different styles of writing. Choose the one that suits you best.
Are you editing the file tutorial.bsl? Good. First, declare the "main" function for the level by typing "<tt>func void main(void)</tt>" at the very beginning of the file. Inside of curly braces, type this one command: <tt>[[chr_teleport]] 0 7045</tt>. This function call will teleport Konoko (her ID is always 0) to flag 7045, which is located in the final training room.


The types are :
The file's contents should look like this:
func void main(void)
{
    chr_teleport 0 7045
}


Shell-Style:
Save the file, open up Oni and click "New Game" (you can also click "Load Game" and choose "TCTF Training" or any save point for "Syndicate Warehouse" and get the same result). You should be standing on the desk in the final training room. Now to start populating the level!
This is compiled much like some System-Shells. It is written in a "one-function-call-a-line" format, and is the easiest to begin with if you have had no coding experience, as the code is neatly displayed line after line. The downside of this it, that once you get into more complex scripts or scripts with many function calls, the file becomes very hard to read and very large.


Example: [[Shell-Style]]
==Adding NPCs (AIs)==
Before we begin, notice that there are two types of AIs: AIs created from character resources ([[CHAR]]) and AIs created at runtime. AIs stored in CHARs are spawned by the function call <tt>[[ai2_spawn]] ''name''</tt>, where "name" is the name of the AI. AIs can be created at runtime by the function call <tt>[[chr_create]] ''ai_number'' start</tt>. These AIs do not have combat logic ([[MELE]]) and thus will not attack you physically, but can still use weapons. Below we will only work with CHAR AIs since they are already set up and ready to use.


C-Style:
Now we are going to add 2 enemy AIs to our script that we started in the last section. Open it up, and let's add some enemies for Konoko to fight.
This is compiled much like the popular coding style C. The syntax can be placed into multiple-function lines and functions are divided by a ''';''' . In advanced scripts, this style will allow you to condense your script and the parts will be easier to read/debug. It also allows you to utilize some of the more advanced features easily. The problem with this is, that starting off, it is harder to write if you have had no previous experience with coding.


Example: [[C-Style]]
To recap, it should look like this:
func void main(void)
{
    chr_teleport 0 7045
}


For the purpose of this article, we will use Shell-Style syntax for simplicity.
After the call to <tt>chr_teleport</tt> add the following two commands on separate lines: <tt>ai2_spawn Top_Striker_1</tt> and <tt>ai2_spawn Top_Comguy_1</tt>. These commands tell the game to spawn two characters already named in the game data, Top_Striker_1 and Top_Comguy_1.


Since these characters do not spawn in the final room they must be teleported there. This time we'll use the <tt>chr_teleport</tt> command to teleport the 2 AIs, and we'll use their names to identify them instead of numeric IDs. Teleport them to the flags "7008" and "7009" respectively. When you're done, main() should now look like this:


----
func void main(void)
{
    chr_teleport 0 7045
    ai2_spawn Top_Striker_1
    ai2_spawn Top_Comguy_1
    chr_teleport Top_Striker_1 7008
    chr_teleport Top_Comguy_1 7009
}


== Part 2: Starting Off ==
The script will now teleport you into the final training room and face you off against two [[Striker]]s. Try it out.


Now, I suggest reading the original script files to get a basic idea for how scripts are laid out. This will allow us to jump-start a bit.
This fight too easy? In the next section we will spice up the fight. P.S.: You'll notice that one of the two Strikers is called a [[Comguy]]. There's no guarantee that Bungie West labeled their AIs correctly. Trust no one.


Open up your IMGD and look for the following folder: EnvWarehouse.
==Giving characters weapons==
Open it up, and we'll take a look at how it it laid out. There should be several files in here. Open using NOTEPAD, the one titled '''warehouse_main.bsl'''. It should look like this:
Giving characters weapons is very easy: simply use the command <tt>[[chr_giveweapon]]</tt>. See [[BSL:Inventory management|HERE]] for the list of weapon names. Let's go with <tt>w3_phr</tt>, the plasma rifle. Give the two Strikers this weapon by adding the commands <tt>chr_giveweapon Top_Striker_1 w3_phr</tt> and <tt>chr_giveweapon Top_Comguy_1 w3_phr</tt> to the main() function. Try out the fight again.


==Spawning weapons on the ground==
Spawning weapons is done with the command <tt>[[weapon_spawn]]</tt>. Change your previous <tt>chr_giveweapon</tt> commands into two <tt>[[weapon_spawn]]</tt> commands in order to spawn plasma rifles at flags 7008 and 7009. You should end up with the commands <tt>weapon_spawn w3_phr 7008</tt> and <tt>weapon_spawn w3_phr 7009</tt>. This will place plasma rifles at each Striker's feet. Each spawned weapon comes with a full clip. An AI will often pick up a weapon with ammo or a weapon which they have ammo for. Try out the fight. You may need to coax the AIs to run past the weapons in order for them to pick them up.


==Preventing scripting overload==
Before we continue, we have a boring matter of some housekeeping to attend to. There are trigger volumes in this room which can fire events during training: one is for supplying the player with fresh ammo if they run out and one is for monitoring whether the glass targets are shot out. Normally the level's scripting will respond to and then disable these "TV"s, but because we wiped out that level scripting, the TVs will continually fire until they overwhelm the scripting system. This is marked by the message "FATAL SCRIPTING ERROR: exceeded maximum scripting parameter base size of 32768" which is visible when [[Developer Mode]] is active. If you have Dev Mode enabled (through the [[Daodan DLL]]), press F1 to bring up the Comlink screen and type 'x' or "thedayismine" to enable Dev Mode, then play the level for about half a minute to see this message appear. This overload doesn't matter for this script yet because it runs all its commands at level start, but for a normal level script which plays out over time, it would disable all commands issued after the overload occurs. To fix this, call <tt>[[trigvolume_enable]] tv74 0</tt> and <tt>trigvolume_enable tv75 0</tt>.


<tt>
==Giving characters powerups==
func void main(void)<br>
Giving characters powerups is much like giving characters weapons. Use the command <tt>[[chr_givepowerup]]</tt> to give a character a powerup.
{<br>
    env_show 2010 0<br>
    gl_fog_blue=.15<br>
    gl_fog_red=.15<br>
    gl_fog_green=.15<br>
    gl_fog_start=.99<br>
    gs_farclipplane_set 5000<br>
    obj_create 20 20<br>
    level_start<br>
<br>
}</tt>


This is an example of a '''main''' file. It contains the information of how the level starts.
The powerups you can give a character are:
* <tt>shield</tt>: Gives a force shield
* <tt>invis</tt>: Gives a phase cloak
* <tt>ammo</tt>: Gives a ballistic ammo clip
* <tt>cell</tt>: Gives an energy cell


Ignore the contents of the script for now, and notice that the contents of the function <tt>main</tt> are enclosed by a set of brackets ('''{''' and '''}''').
Use this command to grant an energy cell to each Striker. You should end up with the commands <tt>chr_givepowerup Top_Striker_1 cell 1</tt> and <tt>chr_givepowerup Top_Comguy_1 cell 1</tt>. This will allow them to reload their weapons once. To review, your script should now say:
func void main(void)
{
  chr_teleport 0 7045
  ai2_spawn Top_Striker_1
  ai2_spawn Top_Comguy_1
  chr_teleport Top_Striker_1 7008
  chr_teleport Top_Comguy_1 7009
  weapon_spawn w3_phr 7008
  weapon_spawn w3_phr 7009
  trigvolume_enable tv74 0
  trigvolume_enable tv75 0
  chr_givepowerup Top_Striker_1 cell 1
  chr_givepowerup Top_Comguy_1 cell 1
}


All functions must be enclosed in this set of brackets or it will not be executed.
==Spawning powerups on the ground==
Spawning powerups is easy with the command <tt>[[powerup_spawn]]</tt>. Add the commands <tt>powerup_spawn invis 7008</tt> and <tt>powerup_spawn hypo 7009</tt>. This command will spawn a phase cloak and a hypo at the Strikers' feet, but since AIs will not pick up powerups, these are for you only.


Close the file, and rename the folder "EnvWarehouse" "EnvWarehouse_original". Then, create a folder called "EnvWarehouse" and create a script file inside and name it "Tutorial". We will be using this file to show various examples of scripts and finally, write a small script of your own.
==Adding an ally==
Adding extra characters to the script can be done in the same way you added the Strikers. You can see all the AI names for this level [http://ssg.oni2.net/subfold/charas/l00_01.htm here]. Look up the name for Griffin in this level (he's available because he appears in Chapter 1's opening cutscene) and spawn him at flag 7007. You should end up with <tt>ai2_spawn griffin</tt> and <tt>chr_teleport griffin 7007</tt>. Your boss will now assist you in the fight against the armed Strikers.


==Making an enemy==
Look back at ssg's table to see what teams the AIs are on. Most of the time, adding enemies simply involve spawning and teleporting the AI to your location, and as long as its team is "Syndicate" it will be an enemy. If you want to make an enemy from someone who would normally be on an allied team, use the <tt>[[chr_changeteam]]</tt> command to change their team to Syndicate like this:


----
chr_changeteam griffin Syndicate


== Part 3: Basic Commands ==
The same principle would work in reverse if you spawned an AI who's on team Syndicate and changed their team to "TCTF" or "Konoko" with this command.


This is just a small section explaining the commands that will be used in this tutorial. For a complete list of commands, see [http://www.fh-eberswalde.de/user/dkriesch/onistuff/commands.htm Ssg's Site] for more information.
==Overpower==
The fight is becoming too slanted against Konoko; let's give her a helping hand. We can use the <tt>[[chr_set_health]]</tt> command to set a character's health points (HP). Note that each character has a base health that is unique to them. For example, Konoko's base HP is 200 while a green Striker's might be only 50. Keep this in mind when setting characters' health. Setting it over their base HP will work like using a hypo at full health. A character in an "overhealth" state will lose 1 HP every 10 ticks (1/6 of a second), whether they're the player character or an AI. However only Konoko and Muro get stronger and glow while they have overhealth. Let's set Konoko's health to 400 so she enters overpower mode: <tt>chr_set_health 0 400</tt>.


ai2_spawn (name) - creates and starts an AI from a character object
Let's recap. If you've added everything above, you should have:
chr_teleport (name) (flag) - teleports a character to a flag
func void main(void)
chr_givepowerup (name) (powerup) (amount) - gives a character a powerup
{
.
    chr_teleport 0 7045
.
.
    ai2_spawn Top_Striker_1
    ai2_spawn Top_Comguy_1
    chr_teleport Top_Striker_1 7008
    chr_teleport Top_Comguy_1 7009
    weapon_spawn w3_phr 7008
    weapon_spawn w3_phr 7009
    trigvolume_enable tv74 0
    trigvolume_enable tv75 0
    chr_givepowerup Top_Striker_1 cell 1
    chr_givepowerup Top_Comguy_1 cell 1
    powerup_spawn invis 7008
    powerup_spawn hypo 7009
    ai2_spawn griffin
    chr_teleport griffin 7007
    chr_changeteam griffin Syndicate
    chr_set_health 0 400
}


==Delays==
To wait for a fixed amount of time before executing a command, use <tt>[[sleep]]</tt>, which takes an argument in ticks (1/60 of a second). Add this to wait for 5 seconds before spawning a training drone:


----
sleep 300
ai2_spawn dum_train_block
chr_teleport dum_train_block 7010


== Part 4: First Script ==
You can also wait for a character's health to reach a certain target before proceeding. Change your <tt>sleep</tt> command to this <tt>[[chr_wait_health]]</tt> command in order to wait for the first Striker to die before spawning the drone:


This section will teach you how to write a very basic arena script (fighting script). It will create an enemy for you to fight and teleport you to confront of him.
chr_wait_health Top_Comguy_1 0
ai2_spawn dum_train_block
chr_teleport dum_train_block 7010


Let's open up our "Tutorial.bsl" file and start typing.
==Separate functions==
It's bad practice to put all your level scripting code in main(). Using multiple functions is not only clearer but allows you to run different code in parallel. Create a new function called "start_fight" and move all the code from main() into start_fight(). Then call start_fight() from main(). It should look like this:


First, declare it the "main" portion of the script by typing "<tt>func void main(void)</tt> at the very beginning of the file. This tells the game which function it is. The "main" function is the function that the game looks at first to determine what is to be executed.
func void main(void)
{
    start_fight
}
func start_fight
{
    ...
}


Below that line, type '''{'''. This tells the game that the function has started.
…with "..." being whatever you had in main() before.


.
==Forking==
.
Now that you get the basic idea of functions, we can use multiple functions to run code in parallel. Change tutorial.bsl to read:
.


MORE TO COME!
func void main(void)
                                                              - Your_Mom
{
    start_fight
    fork spawn_dummy
    sound_music_start mus_ot 0.75
    fork kill_music
}
func start_fight
{
    chr_teleport 0 7045
    ai2_spawn Top_Striker_1
    ai2_spawn Top_Comguy_1
    chr_teleport Top_Striker_1 7008
    chr_teleport Top_Comguy_1 7009
    weapon_spawn w3_phr 7008
    weapon_spawn w3_phr 7009
    trigvolume_enable tv74 0
    trigvolume_enable tv75 0
    chr_givepowerup Top_Striker_1 cell 1
    chr_givepowerup Top_Comguy_1 cell 1
    powerup_spawn invis 7008
    powerup_spawn hypo 7009
    ai2_spawn griffin
    chr_teleport griffin 7007
    chr_changeteam griffin Syndicate
    chr_set_health 0 400
}
func spawn_dummy
{
    chr_wait_health Top_Comguy_1 0
    ai2_spawn dum_train_block
    chr_teleport dum_train_block 7010
}
func kill_music
{
    chr_wait_health Top_Striker_1 0
    chr_wait_health Top_Comguy_1 0
    chr_wait_health dum_train_block 0
    chr_wait_health griffin 0
    sound_music_volume mus_ot 0 5.0
}
 
First main() calls start_fight(), which runs in its entirety before returning control to main(), and then main() forks spawn_dummy(), which waits until one Striker is dead before spawning the training dummy.
 
So far there's nothing different from how the script ran before, but note the third line in main(), which calls [[sound_music_start]]. When you play the level, the music begins immediately. If we had called spawn_dummy() without "fork", we would be stuck waiting for the dummy to spawn before control returned to main() and it could play the music.
 
After that, we fork one more function, kill_music(), which waits for all four characters to be defeated before fading out the music. We could have simply called this function without "fork" since it's the last line of code in main(), but forking it is good practice so that more code can be added to main() in the future without needing to remember to add "fork" to this line.
 
==Save points==
As a final note, remember how TCTF Training and Syndicate Warehouse are the same level, just separated by save point? If you look back at the original main() function that was in warehouse_main.bsl, its last line called the function level_start(). You'll find this function in warehouse_level_scripts.bsl. This function runs whenever a save point from either Training or Warehouse is loaded.
 
func void level_start(string ai_name)
{
    if (save_point eq 0)
    {
      ...
    }
    if (save_point eq 1)
    {
      ...
    }
    ...
}
 
<tt>[[save_point]]</tt> is a built-in variable that can be read but not set, and which tells you the save point that the player loaded, so you can set up your level accordingly (clicking on the name of the level in Load Game registers as save point 0). To record the player reaching a save point in your level, you call <tt>[[save_game]] ''n''</tt>, where 'n' is 1-10.
 
To handle the player restoring a saved game, you use logic like the snippet above to read <tt>save_point</tt> and then set up the level accordingly by doing things such as turning trigger volumes on/off, spawning additional AIs, and whatever other changes are necessary to take the level from its default starting state to the state it should have as of the save point from which the player is loading. After making these changes, you call <tt>[[restore_game]]</tt> to restore the player's state. This function retrieves the player state that was saved in [[persist.dat]] for the current save point: Konoko's position in a level, her health, and her inventory. persist.dat does not store information on world state, which is why you are responsible for handling that manually with your own BSL calls.
 
[[Category:Modding tutorials]]

Latest revision as of 20:40, 18 May 2024

This page is a short tutorial for those who have no experience with scripting on how to get started creating a new level script. It's recommended that you read the BSL:Introduction page first to get a grasp of BSL's syntax. Each significant command in this tutorial should have a link behind it the first time it's referenced; click that link to learn more about the command. For a complete list of commands, see BSL:Functions and BSL:Variables.

The "main" function

Open up your IGMD folder and look for EnvWarehouse/. Inside this folder you should see seven .bsl files. Using a plain-text editor (such as Notepad or Notepad++ in Windows, and TextEdit or BBEdit in macOS), open the file warehouse_main.bsl. It should look like this:

#
# warehouse_main.bsl
#

var int my_save_point;

func void main(void)
{
    env_show 2010 0
    gl_fog_blue=.15
    gl_fog_red=.15
    gl_fog_green=.15
    gl_fog_start=.99
    gs_farclipplane_set 5000
    obj_create 20 20
    level_start
}

This is an example of a level's main script. It contains the main() function, which is always where the level starts. The script file can technically be called anything, but there must be a function called "main" in one of the level's script files. Oni will always look for and start with this function when a level loads.

Creating a script

Close warehouse_main.bsl if you still have it open, and rename the containing folder "EnvWarehouse-original". Create a new folder called "EnvWarehouse" and create a new plain-text file inside it called "tutorial.bsl".

First we're just going to place you in the final room of TCTF Training where you practice firing weapons. The room will be empty since no enemies have been spawned by the script. In case you're wondering how you could be in TCTF Training when this is a script inside EnvWarehouse/, be aware that CHAPTER 00 . COMBAT TRAINING is actually part of the same level data files as CHAPTER 01 . TRIAL RUN and only appears to be separate in the Load Game menu (this is the only time Oni combines two levels into one set of files).

Are you editing the file tutorial.bsl? Good. First, declare the "main" function for the level by typing "func void main(void)" at the very beginning of the file. Inside of curly braces, type this one command: chr_teleport 0 7045. This function call will teleport Konoko (her ID is always 0) to flag 7045, which is located in the final training room.

The file's contents should look like this:

func void main(void)
{
   chr_teleport 0 7045
}

Save the file, open up Oni and click "New Game" (you can also click "Load Game" and choose "TCTF Training" or any save point for "Syndicate Warehouse" and get the same result). You should be standing on the desk in the final training room. Now to start populating the level!

Adding NPCs (AIs)

Before we begin, notice that there are two types of AIs: AIs created from character resources (CHAR) and AIs created at runtime. AIs stored in CHARs are spawned by the function call ai2_spawn name, where "name" is the name of the AI. AIs can be created at runtime by the function call chr_create ai_number start. These AIs do not have combat logic (MELE) and thus will not attack you physically, but can still use weapons. Below we will only work with CHAR AIs since they are already set up and ready to use.

Now we are going to add 2 enemy AIs to our script that we started in the last section. Open it up, and let's add some enemies for Konoko to fight.

To recap, it should look like this:

func void main(void)
{
    chr_teleport 0 7045
}

After the call to chr_teleport add the following two commands on separate lines: ai2_spawn Top_Striker_1 and ai2_spawn Top_Comguy_1. These commands tell the game to spawn two characters already named in the game data, Top_Striker_1 and Top_Comguy_1.

Since these characters do not spawn in the final room they must be teleported there. This time we'll use the chr_teleport command to teleport the 2 AIs, and we'll use their names to identify them instead of numeric IDs. Teleport them to the flags "7008" and "7009" respectively. When you're done, main() should now look like this:

func void main(void)
{
   chr_teleport 0 7045

   ai2_spawn Top_Striker_1
   ai2_spawn Top_Comguy_1

   chr_teleport Top_Striker_1 7008
   chr_teleport Top_Comguy_1 7009
}

The script will now teleport you into the final training room and face you off against two Strikers. Try it out.

This fight too easy? In the next section we will spice up the fight. P.S.: You'll notice that one of the two Strikers is called a Comguy. There's no guarantee that Bungie West labeled their AIs correctly. Trust no one.

Giving characters weapons

Giving characters weapons is very easy: simply use the command chr_giveweapon. See HERE for the list of weapon names. Let's go with w3_phr, the plasma rifle. Give the two Strikers this weapon by adding the commands chr_giveweapon Top_Striker_1 w3_phr and chr_giveweapon Top_Comguy_1 w3_phr to the main() function. Try out the fight again.

Spawning weapons on the ground

Spawning weapons is done with the command weapon_spawn. Change your previous chr_giveweapon commands into two weapon_spawn commands in order to spawn plasma rifles at flags 7008 and 7009. You should end up with the commands weapon_spawn w3_phr 7008 and weapon_spawn w3_phr 7009. This will place plasma rifles at each Striker's feet. Each spawned weapon comes with a full clip. An AI will often pick up a weapon with ammo or a weapon which they have ammo for. Try out the fight. You may need to coax the AIs to run past the weapons in order for them to pick them up.

Preventing scripting overload

Before we continue, we have a boring matter of some housekeeping to attend to. There are trigger volumes in this room which can fire events during training: one is for supplying the player with fresh ammo if they run out and one is for monitoring whether the glass targets are shot out. Normally the level's scripting will respond to and then disable these "TV"s, but because we wiped out that level scripting, the TVs will continually fire until they overwhelm the scripting system. This is marked by the message "FATAL SCRIPTING ERROR: exceeded maximum scripting parameter base size of 32768" which is visible when Developer Mode is active. If you have Dev Mode enabled (through the Daodan DLL), press F1 to bring up the Comlink screen and type 'x' or "thedayismine" to enable Dev Mode, then play the level for about half a minute to see this message appear. This overload doesn't matter for this script yet because it runs all its commands at level start, but for a normal level script which plays out over time, it would disable all commands issued after the overload occurs. To fix this, call trigvolume_enable tv74 0 and trigvolume_enable tv75 0.

Giving characters powerups

Giving characters powerups is much like giving characters weapons. Use the command chr_givepowerup to give a character a powerup.

The powerups you can give a character are:

  • shield: Gives a force shield
  • invis: Gives a phase cloak
  • ammo: Gives a ballistic ammo clip
  • cell: Gives an energy cell

Use this command to grant an energy cell to each Striker. You should end up with the commands chr_givepowerup Top_Striker_1 cell 1 and chr_givepowerup Top_Comguy_1 cell 1. This will allow them to reload their weapons once. To review, your script should now say:

func void main(void)
{
  chr_teleport 0 7045

  ai2_spawn Top_Striker_1
  ai2_spawn Top_Comguy_1

  chr_teleport Top_Striker_1 7008
  chr_teleport Top_Comguy_1 7009

  weapon_spawn w3_phr 7008
  weapon_spawn w3_phr 7009

  trigvolume_enable tv74 0
  trigvolume_enable tv75 0

  chr_givepowerup Top_Striker_1 cell 1
  chr_givepowerup Top_Comguy_1 cell 1
}

Spawning powerups on the ground

Spawning powerups is easy with the command powerup_spawn. Add the commands powerup_spawn invis 7008 and powerup_spawn hypo 7009. This command will spawn a phase cloak and a hypo at the Strikers' feet, but since AIs will not pick up powerups, these are for you only.

Adding an ally

Adding extra characters to the script can be done in the same way you added the Strikers. You can see all the AI names for this level here. Look up the name for Griffin in this level (he's available because he appears in Chapter 1's opening cutscene) and spawn him at flag 7007. You should end up with ai2_spawn griffin and chr_teleport griffin 7007. Your boss will now assist you in the fight against the armed Strikers.

Making an enemy

Look back at ssg's table to see what teams the AIs are on. Most of the time, adding enemies simply involve spawning and teleporting the AI to your location, and as long as its team is "Syndicate" it will be an enemy. If you want to make an enemy from someone who would normally be on an allied team, use the chr_changeteam command to change their team to Syndicate like this:

chr_changeteam griffin Syndicate

The same principle would work in reverse if you spawned an AI who's on team Syndicate and changed their team to "TCTF" or "Konoko" with this command.

Overpower

The fight is becoming too slanted against Konoko; let's give her a helping hand. We can use the chr_set_health command to set a character's health points (HP). Note that each character has a base health that is unique to them. For example, Konoko's base HP is 200 while a green Striker's might be only 50. Keep this in mind when setting characters' health. Setting it over their base HP will work like using a hypo at full health. A character in an "overhealth" state will lose 1 HP every 10 ticks (1/6 of a second), whether they're the player character or an AI. However only Konoko and Muro get stronger and glow while they have overhealth. Let's set Konoko's health to 400 so she enters overpower mode: chr_set_health 0 400.

Let's recap. If you've added everything above, you should have:

func void main(void)
{
   chr_teleport 0 7045

   ai2_spawn Top_Striker_1
   ai2_spawn Top_Comguy_1

   chr_teleport Top_Striker_1 7008
   chr_teleport Top_Comguy_1 7009

   weapon_spawn w3_phr 7008
   weapon_spawn w3_phr 7009

   trigvolume_enable tv74 0
   trigvolume_enable tv75 0

   chr_givepowerup Top_Striker_1 cell 1
   chr_givepowerup Top_Comguy_1 cell 1

   powerup_spawn invis 7008
   powerup_spawn hypo 7009

   ai2_spawn griffin
   chr_teleport griffin 7007
   chr_changeteam griffin Syndicate

   chr_set_health 0 400
}

Delays

To wait for a fixed amount of time before executing a command, use sleep, which takes an argument in ticks (1/60 of a second). Add this to wait for 5 seconds before spawning a training drone:

sleep 300
ai2_spawn dum_train_block
chr_teleport dum_train_block 7010

You can also wait for a character's health to reach a certain target before proceeding. Change your sleep command to this chr_wait_health command in order to wait for the first Striker to die before spawning the drone:

chr_wait_health Top_Comguy_1 0
ai2_spawn dum_train_block
chr_teleport dum_train_block 7010

Separate functions

It's bad practice to put all your level scripting code in main(). Using multiple functions is not only clearer but allows you to run different code in parallel. Create a new function called "start_fight" and move all the code from main() into start_fight(). Then call start_fight() from main(). It should look like this:

func void main(void)
{
   start_fight
}

func start_fight
{
   ...
}

…with "..." being whatever you had in main() before.

Forking

Now that you get the basic idea of functions, we can use multiple functions to run code in parallel. Change tutorial.bsl to read:

func void main(void)
{
   start_fight
   fork spawn_dummy
   sound_music_start mus_ot 0.75
   fork kill_music
}

func start_fight
{
   chr_teleport 0 7045

   ai2_spawn Top_Striker_1
   ai2_spawn Top_Comguy_1

   chr_teleport Top_Striker_1 7008
   chr_teleport Top_Comguy_1 7009

   weapon_spawn w3_phr 7008
   weapon_spawn w3_phr 7009

   trigvolume_enable tv74 0
   trigvolume_enable tv75 0

   chr_givepowerup Top_Striker_1 cell 1
   chr_givepowerup Top_Comguy_1 cell 1

   powerup_spawn invis 7008
   powerup_spawn hypo 7009

   ai2_spawn griffin
   chr_teleport griffin 7007
   chr_changeteam griffin Syndicate

   chr_set_health 0 400
}

func spawn_dummy
{
   chr_wait_health Top_Comguy_1 0
   ai2_spawn dum_train_block
   chr_teleport dum_train_block 7010
}

func kill_music
{
   chr_wait_health Top_Striker_1 0
   chr_wait_health Top_Comguy_1 0
   chr_wait_health dum_train_block 0
   chr_wait_health griffin 0
   sound_music_volume mus_ot 0 5.0
}

First main() calls start_fight(), which runs in its entirety before returning control to main(), and then main() forks spawn_dummy(), which waits until one Striker is dead before spawning the training dummy.

So far there's nothing different from how the script ran before, but note the third line in main(), which calls sound_music_start. When you play the level, the music begins immediately. If we had called spawn_dummy() without "fork", we would be stuck waiting for the dummy to spawn before control returned to main() and it could play the music.

After that, we fork one more function, kill_music(), which waits for all four characters to be defeated before fading out the music. We could have simply called this function without "fork" since it's the last line of code in main(), but forking it is good practice so that more code can be added to main() in the future without needing to remember to add "fork" to this line.

Save points

As a final note, remember how TCTF Training and Syndicate Warehouse are the same level, just separated by save point? If you look back at the original main() function that was in warehouse_main.bsl, its last line called the function level_start(). You'll find this function in warehouse_level_scripts.bsl. This function runs whenever a save point from either Training or Warehouse is loaded.

func void level_start(string ai_name)
{
   if (save_point eq 0)
   {
      ...
   }

   if (save_point eq 1)
   {
      ...
   }

   ...
}

save_point is a built-in variable that can be read but not set, and which tells you the save point that the player loaded, so you can set up your level accordingly (clicking on the name of the level in Load Game registers as save point 0). To record the player reaching a save point in your level, you call save_game n, where 'n' is 1-10.

To handle the player restoring a saved game, you use logic like the snippet above to read save_point and then set up the level accordingly by doing things such as turning trigger volumes on/off, spawning additional AIs, and whatever other changes are necessary to take the level from its default starting state to the state it should have as of the save point from which the player is loading. After making these changes, you call restore_game to restore the player's state. This function retrieves the player state that was saved in persist.dat for the current save point: Konoko's position in a level, her health, and her inventory. persist.dat does not store information on world state, which is why you are responsible for handling that manually with your own BSL calls.