Rise of Legends Scenario Design - Become a Scripting Legend
By Archaeopetrix of Ambition Designs
Scenario Design Workshop
Becoming a Scripting Legend
Or at least a decent
beginner
By Archaeopterix.
| Variables |
Creation and
assignment | Functions |
Callbacks | Heirachy | Operators |
A script looks awefully scary at first sight, but it is delicious
to work with once you know how to. In this article I hope to provide you a good
start on understanding scripting for Rise of Legends. I will try to do so by
means of the script file of a scenario from the Rise of Legends campaign: the
Vinci scenario Battle at the Ranconi Bridge.bhs. Why that one? It is one of the
shortest scripts, and more fun to analyse than some boring example
script.
Scripts are for Rise of Legends what the regular "triggers" were
in the "Age of" series, and more. Scripts for scenarios are written in the
JavaScript language and are contained in .bhs files. You can write such a file in NotePad
or any other plain text editor. I use ConTEXT myself because it is fast, has a
great interface, and it supports so-called syntax-highlighters for many
languages, including JavaScript! A syntax-highlighter gives the script you're
viewing or working on nice colours to make it easier to read. So, you can get
ConTEXT here, but there are many other good
editors.
When you've written a script and saved it as a .bhs file, you
can go to the scenario editor in Rise of Legends and load your map. Then, you go
to the scripting tools and add the .bhs file or files to your scenario. Once
you've done that, the script file is "compiled" and, so to say, copied into the
map file itself. After that, you can throw the .bhs file away if you like (but
don't*), because it is contained in the scenario file itself. This is also what
the scenario designers at BHG did, and it is the reason you won't find any .bhs
scripts in the campaign folder at first.
*Note: I assume it all works
like this, but I am by no means sure. Plus it's always good to have a
backup.
However, the BHG guys have been nice to us designers. To be able
to view the scripts BHG used for the campaign's scenarios, go to your main Rise
of Legends folder (the one in Program Files, not in My Documents) and launch the
file ModPack. The file will be unpacked and many .bhs files will appear in the
campaign folder and subfolders! Having done that, let's go to the Vinci folder,
then to Maps, and from there open the Battle at the Ranconi Bridge.bhs file.
Notice that there is also the .map file, which is the map file itself (simply
put this contains where all the trees are, which terrain types are where, etc)
written in .XML. The latter however is none of our business. In summary, open
the following file in a suitable text editor:
C:\Program Files\Microsoft
Games\Rise Of Legends\campaigns\Vinci\maps\Battle at the Ranconi
Bridge.bhs
Make sure to select a JavaScript syntax-highlighter if
available. In ConTEXT, click JavaScript from the dropdown box in the toolbar.
So, how does it feel? You probably only recognise a few words here and there,
but we'll change that. However, I'm not going to analyse each and every word in
the script - I will teach you how to do so yourself.
The first
thing you see is this:
...
include "..\..\Campaigns Script
Library.bhs"
int function sf_blow_up_bridge();
int
bridge_blown;
...
The first line imports a different script file into
this one. That script file, the Campaigns Script Library, contains some handy
things that are used often in the campaign. Instead of writing those things all
over again in each script, you just type them once in a seperate file and add
"include ..." to each file where you need them. Which things this library
contains is up to you to explore, after you finish this guide. It is
unimportant for now.
Next are these two things with "int" in front of it.
Int stands for the data type "integer" - a whole number. There are different
types, "double" containing decimal numbers too, or String containing a series of
keyboard characters, like "apple juice"! Before we continue to go down the
script, I will explain what these types are and what you need them for. We will
put the script aside for a while, but don't worry!
Variables
In all programming languages, you don't
work with real existing things alone, such as the number "2" or a word "monkey".
The computational power of those languages comes from a different thing: so
called "variables" that can contain those real existing things like "2"
or "monkey". For those of you who had maths, this is very basic, but I'll give
an example anyway:
6 = 5 + X.
Here, X represents a variable,
because it isn't a real value. However, you can calculate X by saying "Hmm, to
get six when I have only five, I have to add one. The variable X must equal 1!".
Easy eh? Of course you can work with multiple variables in one line,
like:
Y = 5 + X.
Now you can say things like: If X equals 3, then
Y equals 8 (for 5+3=8)! But if I set X to 1, Y will equal 6! Y's value depends
on the value of X, and that's where the great computational power of
programming languages comes from.
Now, in the above examples, it was
quite clear that the letters X and Y stood for numbers. However, your computer
doesn't have a thing like "intuition" that we humans have. It needs to know
exactly which value types can be put in a variable, and which value types
cannot. A variable made for numbers can never contain a letter or word, or
a piece of fruit - and vice versa. There is a bunch of types, the "primitive
types", that you can use for variables in JavaScript. There are more, but
the scripting manual says not all are supported by RoL... I wouldn't believe
that though, but these are the important ones anyway:
- int (integer): may
only contain a whole number, positive and negative. In example: -3, -29384, 0,
2938, 1.
- float: may contain whole or decimal numbers, positive and
negative. In example: 1.5, -283.34562, 0, -23.
- boolean: may contain a truth
value, so only either true or false. It can be seen as a switch that can be
turned on or off (1 or 0 for you digital computer geeks).
- String*: may
contain any series of keyboard characters. Correct strings are "abcde", "ab ˝#
zd&%a sd" and "123asdh565ja" - basically any key you can hit on your
keyboard, enclosed in two "double quotes", may form a string.
*String is
in fact not a primitive type, but a so called "literal". Why exactly is beyond
the scope of this guide, but a dummy explanation is that once a String has been
created, you cannot change the value anymore by adding or substracting letters.
For Rise of Legends purposes, considering a string as a type works fine though.
Mind that String, because it is not a real type, is the only "type" that begins
with a capital letter!
In addition, the BHG guys defined some nice new
RoL-related types, so called "cast types" for Rise of Legends use only! I copied
these from the scripting manual by BHG (don't read them all through, just scan
the list to get the idea):
- Unit can keep track of an individual unit
id.
- UnitType is a specific type of unit, like “Imperial Musketeer.”
-
UnitGroup keeps track of a group of unit ids.
- Build can keep track of an
individual building id.
- BuildType is a specific type of building, like
“Barracks.”
- BuildGroup keeps track of a group of building ids.
-
Location keeps track of a specific point on the map.
- Region keeps track of
a region of space on the map.
- Timer is used by the script to keep track of
a timed event in seconds.
- CastTimer is a special type of timer that is
defined inside the scenario editor.
- Path keeps track of a collection of
locations in a direction.
- City keeps track of a specific city on the
map.
- Leader keeps track of a specific player. “1”, “2”, “3”, …, “8” are
valid.
- SpellType keeps track of a specific type of spell, such as “Summon
Army 1”.
- Quest is used to reference a specific quest
- SubQuest is used
to reference a subquest of a quest.
The great thing is that you can
define many of these things in the scenario editor itself. Using the "scripting
tools" available you can define and name groups of units, locations, paths,
areas, etcetera, and you can simply refer to them in the script by using those
names! We will first do some stuff with the primitive types (int, boolean, etc),
and have a look at the cast types (Unit, UnitGroup, etc) later when we're
looking at the script again - so forget about those, for now.
Creation and assignment
Now, we were talking about
variables. Variables are created, and then a value is assigned to them. How do
you create a variable of a certain type? Creating it reserves a little bit of
memory and gives it a name for future reference. Each bit of memory created like
that has to be of a certain type and will only contain values of that type. You
create a variable of a certain type simply by stating [TYPE] [NAME];, replacing
[TYPE] with a primitive (or cast) type and [NAME] with the desired name. Let's
create some variables!
...
int x;
String y;
...
This
reserves a piece of memory called "x" for integer-type values, and another one
called "y" which will contain string type values. The names x and y are totally
random: you could as well have picked the names "monkey" or "poop12345". In
general, pick a name that suits you and that displays what the variable is for.
By the way, notice how in JavaScript every statement ends with a ";". Anyway,
after creating a variable, you can assign values to it. You do this with the
assignment operator "=". Notice that "=" pretty much stands for "becomes" - it
is not the "equals" check that you learned at school. Example:
...
x =
5;
y = "apple juice";
...
This assigns the number 5 to the
previously created integer type space "x" (in short: "x becomes 5"), and the
value "apple juice" to the previously created string type scace "y". Notice that
the value type you want to assign to it has to correspond to the variable type!
The following will cause an error:
...
int x;
x = "apple
juice";
...
This crashes because a string cannot be stored in a
variable created for integers. Furthermore, notice that each variable name (x
and y in this example) can only be used once. The following will cause an error
"x already defined":
...
int x;
int x;
..
And the
following will cause the same error:
...
int x;
String
x;
...
So, types have to correspond at any time, and names may only be
used once - regardless of the type! Now, specified values aren't the only things
you can assign to variables. You can also assign variables to variables! Check
this:
...
int x;
int y;
x = 5;
y = x;
...
The
integer "y" will now also contain the value "5". Notice that again the
types need to correspond. You cannot assign a String variable to an integer. One
more thing about creation and assignment is a handy shortcut: you can create
variables and assign values to them at once. Example:
...
int x =
5;
String y = "apple juice";
...
This creates the integer "x" with
value 5, and the string "y" with value "apple juice". So, one of the first
things that happen in the Ranconi Bridge script is that two integers are
created:
...
int function sf_blow_up_bridge();
int
bridge_blown;
...
But no values are assigned to them, yet. That's
probably done later on, when things are actually happening. If you've been
paying attention you will see that "int function sf_blow_up_bridge()" isn't in
accordance with the regular format of creating variables. The type is integer,
yes, but it contains another weird word: "function". This means that
sf_blow_up_bridge() is not going to be a set variable, but a function that will
be defined later. By any means, do not continue before you understand how
creation and assignment work.
Functions
So, put the script aside again. Here's a
dummy explanation of what a function is. Imagine you need to remember a random
list of numbers, say 173847. Now, you take your friend Johnny and ask him to
remember the sequence 847. You'll then only have to remember "173 johnny" -
assuming that Johnny won't forget his three numbers. "173 johnny" is, you
have to admit, much easier to remember than the full sequence! In this
example, johnny can be seen as a very simple function - a function that
returns the value 847. Now, how are functions used in Javascript?
To be able to illustrate functions in a proper way, it's time for some basic
maths - which you'll need anyway. We want to do some calculations. We can
just use the variable type integer combined with regular symbols for adding
(+), substracting (-), multiplying (*), and dividing (/), and some more
I will not state here. In example:
...
int x = 3+7;
float y = (3.54 * 1.23) /
3;
...
This is all valid. Integer "x" now contains 10, and the
floating point "y" contains some very ugly number with many decimals. Now, a
great thing is that we can also refer to the variable itself when we assign a
value to it. Sounds odd, but have a look at this:
...
int x = 2;
x
= x + 5;
x = x * 3;
...
The program goes through these lines
top-down. So, this script first creates a variable "x" of type integer and
assigns the value 2 to it. Next, it says "x becomes x plus 5", so it adds five
to itself, so x is then 7 (2+5). Next, it says "x becomes x times 3", which
means that x is then 21 (7*3). Very useful, you will see!
Now imagine we
want to do some complicated calculation, and we want to do it several times.
Below, two variables are created and we want them both to obtain the result of a
"complicated" (hey, it's just a dummy guide) calculation.
...
int
x;
int y;
x = ((5+3)/(19/4)) * 3;
y = ((5+3)/(19/4)) *
3;
...
But that is a terrible way to code it. In example we want to
change the number 3 to 4 in the above calculation - we'd have to change it
twice, once for the x and once for the y. This is called redundancy, and
it is the worst thing a script can have. Because when changing, you might forget
one, or make a typo. So, we have to avoid this redundancy so you'll only have to
make that kind of adjustments once. Have a look at this:
...
int function
calculation() { // The function calculation() of type integer.
return
((5+3)/(19/4)) * 3; // Return the result of the
calculation.
}
int x;
int y;
x = calculation();
y =
calculation();
...
We have now created a function: a little part of the script that
kind of stands on it's own, that - just like johnny - returns a certain
value. The function has a name and a type, just like any simple variable
like x and y. The type of the function in the example is integer, and
its name is "calculation()". Note that you have to include the brackets
"()" to indicate that it's a function, not a simple variable! What this
little script does is the following. First it defines the function -
below I'll explain how it works. Then it creates two integers x and y,
and then a value is assigned to both of them. But this value is not a
simple value like 5 or 19283, no, it is a reference to the function
calculation() we defined earlier in the script. So it will go to
calculation(), does whatever it says between the { curly brackets }
and assigns the resulting value to x and y. (Remember Johnny? In
that example you could just say x = johnny, and x would then contain
the value 847 that Johnny had to remember).
In between the curly
brackets of the function, we only see a return statement. You could
as well put lots of other things in there, anything which you can put
outside of a function, but for now the return statement is sufficient
to get what we want. What does it do? Return [something] gives you a
value. In this case, it gives you the result of the calculation, it
gives you the function's (Johnny's) output. Every function
requires such a return statement, and every function has to return
a value of it's own type. So an integer function may never return
"apple juice".
As you can see, if we now want to change the number 3 to 4 in the
calculation, we only have to make one adjustment instead of two. It is no longer
redundant! Make sure you understand the way functions work, and why it is your
weapon against redundancy! If you understand, then let's have a look at a
somewhat more flexible function:
...
int x = 5;
int function add_to_x(int to_be_added)
{
int the_result = x + to_be_added;
return the_result;
}
int y =
add_to_x(7);
...
As you can see for yourself, this script creates the
integer "x" with value 5. Then it defines a function of type integer, called
"add_to_x". Unlike the previous example, this function has something in between
the ( round brackets ). And between those round brackets is the input of
a function. Apparently, the function "add_to_x()" requires an input of type
integer. Then, between the { curly brackets } of the function, we see that it
adds this input (which it called "to_be_added") to the value of "x" defined
earlier, and returns the result. Further down, integer "y" is created, and
assigned to it is the value add_to_x(7). So, the program calls for add_to_x(7),
finds the function, assigns 7 to the integer to_be_added and looks in the {
curly brackets }. It adds it's input (7) to x (5) and returns the result (12).
So, the integer y will have the value 12!
So, functions have an output,
but can also have inputs! In addition, a function can have any type and you
wish, as long as it returns what it's supposed to return (an integer function
should always return an integer value). Also, a function can have as many
inputs as you wish! One last example to illustrate this:
...
int function add_this_to_that(int x, int y) {
return x +
y;
}
...
You should be able to figure out what this function does
by yourself. What happens if I say "int my_integer = add_this_to_that(3,7);"? If
you have no idea, you shouldn't continue this guide but re-read everythingIf you
have no idea, you shouldn't continue this guide but re-read everything above,
step by step - don't panic, sooner or later you'll get it! And when you
understand functions, you understand a very important building block of
scripts! On a sidenote, notice that when you call for a function, you do
not have to specify that it is indeed a function you're calling. Just
mentioning the name (with round brackets) is enough, like
"add_this_to_that(3,7);".
Now, if you payed
attention, you will remember line 3 in the script:
...
int function
sf_blow_up_bridge();
...
This doesn't really mean anything yet. The
function isn't defined yet, you've only told the program that a
definition of this function will follow. Every custom function you are going to
use and define later one, has to be mentioned at the start of the script first
(at least above the first place in the script where you call for it). So,
on line 3 is the first mentioning of a function, now let's see where its
definition can be found - scroll down until you see:
...
int function sf_blow_up_bridge() {
find_unit("trucks",
"2", "Demolition Truck");
Unit the_unit;
int scan;
int trucks_in_region
= 0;
for (scan = 0; scan < unit_group_length("trucks"); scan++)
{
the_unit = unit_at_index("trucks", scan);
if
(is_unit_in_region(the_unit, "bridge_region")) {
trucks_in_region =
trucks_in_region + 1;
}
}
return trucks_in_region;
}
...
Starting on line 286. Here, the function that you
mentioned on line 3, is defined. As you can see, this piece of code has the same
format as the simple integer functions we used to illustrate things earlier in
this chapter. The thing between the { curly brackets } is the part that will
execute when you call for the function. As you can see further, it returns
"trucks_in_region", which has been created before, as an integer. So, the types
match: the function type is int, and it will return an int. Great!
There
is one special type of function: "void" - no int, float, boolean, or String, but
void! What the heck is a void? A void is the type of a thing that doesn't
return any value. It does certain things, but doesn't return anything.
Because thingeys of type void don't return anything, there is no "return;"
statement to be found, unlike in the integer functions above*. Of course voids
can, like the functions we've seen before, have parameters between the ( round
brackets ).
*Note: Sometimes there is a return statement in a void
function, but then it's effect is just that it aborts the function
execution.
Below is an example of a very simple void
function:
...
int x = 3;
void function some_void_function(int
to_be_added){
x = x + 5;
}
...
Now, what happens if you say
"some_void_function(5);"? The number 5 will be added to the variable x. So x
will then become 3+5=8. If you then call the function again, say
"some_void_function(9);", then x will become 8+9=17. And so on. In summary,
voids can be called like any other function but they only do stuff, they
don't return anything. If all the function stuff is clear, let's continue to the
next line in the script, line 6:
...
void solo_callback
on_solo_game_frame () {
...
This is the start of what looks like
another void function, sort of. It's type is void, it has a name, and some part
between curly brackets. However, it isn't a function like the ones we've seen
before. Instead, it is defined as a "solo_callback" - whatever that means. Let's
see how these void "callbacks" are used in the Rise of Legends
script.
Callbacks
Scripts for Rise of Legends are
event-driven. When something happens, the game itself "sends" and event
message to your script, and then the script responds to that. The script can
respond to that through a so-called callback. In example, there is a
callback called "on_spell_cast" that will be activated when a certain player in
the game cast a certain spell. There is one called "on_mouse_down" that will be
activated when you click your mousebutton. There are dozens of callback types,
all to be found in the Script Function Reference Sheet found in BHG's scripting
manual.
Can you guess what the type of these callback things is? It
doesn't return any value, but it surely does stuff. It is a callback of the type
void, of course! Take again the line from the script:
...
void
solo_callback on_solo_game_frame () {
...
This is apparently a
callback, that responds on each game frame. A game frame is the shortest
period of time in a game - it lies in the range of milliseconds. So, you can
pretty much say that this void is always activated, doing stuff, time and time
again, during the entire game. Notice, by the way, that in addition to the
"void" type, callbacks need another "type"-ish thing. Callbacks are either
"game_callback" or "solo_callback". I'm not sure what the difference is, but as
far as I know there is only one callback that only works when defined as a
solo_callback: on_game_frame! Make sure that every single player scenario
contains this one as a solo_callback - the other callbacks it has can be
game_callbacks. Multiplayer game scripts can only contain game_callbacks I
think.
Back to the script. The void callback "on_solo_game_frame()" runs
every frame. It then does everything within its { curly brackets }, and
that's quite a lot. Scroll all the way down until you see the closing bracket }.
Mind that there might be other opening and closing brackets { in the script. The
bracket that closes the entire block belonging to this void is on line 280
(displaying the line numbers is a great feature of many text editors!) - you can
see which closing bracket belongs to which opening bracket by looking at the
amount of space in front of it. Found it? Good. And what do we see there? On
line 282 starts a new void!
...
void game_callback
on_unit_trained(Unit the_unit, Leader the_leader, UnitType the_type, Build
the_build) {
...
Another callback, this time it's the callback
"on_unit_trained()", with several parameters - the variables between ( round
brackets ). Remember that these parameters are the input of a function (scroll
back to the functions part if you've forgotten), or in this case of the
callback.
Now, when does this one launch? It is activated when a player
trains a unit in the game. The unit ID of the created unit is assigned to the
variable "the_unit", the player that created it is assigned to the variable
the_leader. Notice that the types correspond: a Unit type of variable may
contain unit IDs, whereas a Leader type variable may contain "1", "2", "3" etc.
Furthermore, there is the parameter the_type, to which the unit type of
the unit (in example "imperial musketeer") will be assigned, and the parameter
the_build which - I guess - will contain the building ID it was created from.
Within the { curly brackets } that belong to this void, you can refer to these
four variables, but we will see how that is done later on. Let's scroll down to
the closing bracket and see what's beyond on line 308:
...
void
game_callback on_cutscene_finished (String cutscene_name, int was_skipped)
{
...
Another callback! "on_cutscene_finished" it's called, and it has
two variable parameters. This callback will probably run when a cutscene, an
in-game cinematic, is finished. The parameters of this void callback are a
simple string, that will contain the name of the finished cutscene, and an
integer that will probably contain a 1 if the cutscene was skipped by the
player, and -1 if it wasn't - so it's pretty much like a switch that can be
turned on or off.
If you've payed attention during the part about types,
you will see that here the integer in fact acts like a boolean (true/false)
variable. True is equal to the integer 1 and false is equal to the integer -1.
Why didn't they use a regular boolean? Well, the engine of RoL (as do many other
applications) uses the values -1 and 1 for these kind of things, and I guess it
was too much work for BHG to add an automatic converter-to-booleans. Just get
used to integers representing booleans!
Next, on line 333 we see another
callback:
...
void game_callback on_spell_cast(SpellType the_spell,
Unit unit_caster, Leader leader_caster, Location target_loc, Unit target_unit,
Build target_building) {
...
That callback has a shitload of
parameters! But you can guess what it does by looking at the names and types of
the parameters. My guess is that this callback will run when a spell is cast,
and that the parameter variables will contain the spell that is cast, the unit
that cast it, the player (leader) that owned the unit, and the location, the
unit or the building at which it was targeted. Easy eh? Again, we will soon see
how these variables belonging to a callback will be used to actually do
something.
Just for the sake of satisfaction, we scroll down to the last
thing in the script, even though it doesn't particularly belong to this chapter
and though we've examined it before:
...
int function
sf_blow_up_bridge() {
...
Here we see the function that I've already
explained. It is the integer function called "sf_blow_up_bridge". Later, we will
have a look at what it actually does (and what it returns). For now, check line
352. Apparently this integer function returns an integer variable named
"trucks_in_region" - which probably contains a value that represents the amount
of demolition trucks in the bridge region. Just a guess.
Now that we've
reached the end of the script, we know what the biggest building blocks of the
script globally are, and it is time for some more theory. Because when you
create a variable, you cannot call for it on every place in the script. A
variable created in the void "on_cutscene_finished" will only exist there. It
has to do with the [i]hierarchy[/i] of the script...
Hierarchy
A very, very important feature of
JavaScript and most other programming languages is that it is heirarchical. It
isn't a long list of variables and functions with equal power. No, a script
consists of many sub-functions and sub-sub-functions - the functions and
callbacks we've seen before. Where a variable can be seen or accessed is what we
call the "scope" of a variable. A variable can only be seen within it's scope -
outside of it it doesn't exist. Now, what is the scope of a variable? The scope
consists of its own block plus the scopes of its parent blocks. A block is a
piece of code in between the previously seen { curly brackets }.
You
could see this block-hierarchy as a number of levels to which the different
parts of a script belong. When you open a new curly bracket {, you begin a new
lower level. In general, you will see that the more tabs there are in front of
the line, the lower the level. You can look at the variables created in higher
levels, but the variables in lower levels are totally invisible. Take a look at
the following examples of a nonsense function.
...
int x =
3;
int function some_function() {
int y;
y = 7+x; // We can call
"x", because it was created on a higher level.
return y;
}
int z =
y; // now we try to call "y" from a higher level, but this won't
work.
...
The integer "x" can be seen inside the function, because it
was created on a higher level. However, the integer "result" was created inside
the function, which is a lower level, so it doesn't exist outside of it. Another
example:
...
int function some_function() {
int x = 5;
return
x;
}
String x = "This is allowed!";
...
Now, why can the
name "x" be used twice? Isn't that forbidden?! In this case, the first "x", an
integer, was created (and assigned) within the function. When you create the
string "x", you are on a higher level, where the integer "x" doesn't even exist.
Another example:
...
int function some_function() {
int x =
5;
return x;
}
String function some_other_function() {
String x
= "This is allowed";
return x;
}
...
Can you tell me why this is
allowed? And one more:
...
String x = "Apple Juice"
int
function some_function() {
int x; // This isn't allowed!
x = 5;
return
x;
}
...
Why is the latter not allowed? If you know the answers to
the last two questions, you pretty much know the basics of creation, assignment
and scopes. Often, we refer to high-level variables (that can be accessed on all
the lower levels) as "Global variables". Variables created within a low level
function can be called "Local variables". Notice that Global and Local is only
relative. A global variable in your script is a local variable when you look at
the entire computer as a whole. On the other hand, a local variable in some
function is in fact a global variable if you look at the sub-functions of that
function.
However, when we say "global" we usually mean "accessable in
the entire script". In general, you will see most global variables defined at
the first lines in a script. In example, the integer bridge_blown created at the
top in the Ranconi Bridge script can be considered a global variable.
Now
let's have a look at the script again! We know the bigger structure of it, and
we already know a good deal of the JavaScript language, so let's scroll up and
zoom in on the first callback this script has.
On with the script
As mentioned, this callback runs on
every game frame. Therefore, this will be the part of the script in which
you should place the things that, throughout the entire game, check the state of
the game and do things according to it: triggers! But before we get to
the triggers, let us go through the script bit by bit.
... line
7-12
Unit unit_id;
String unit_type;
Location loc;
Location
attack_loc;
int hide_map;
int i;
...
What do we have here? The
creation of variables, of course. A Unit variable called unit_id, a String
containing a unit type, two Location variables, and two integers. No values are
assigned to them yet, but they are already created. Why? Because if you create
them above the rest of the callback, the variables will be accessable in the
entire block between this callback's { curly brackets }. Like global variables
in the entire script, these variables then exist in the entire callback
"on_solo_game_frame()". You will see why this will come in handy later. For now,
just keep in mind that we've got these variables ready for us to initialise and
use!
... line 14-16
static Unit venza_id;
static int
truck_move_time;
static int defense_spawn;
...
Next, some more
variables are created. Another Unit variable and two integers. These variables
have an extra property: they are static. When you do not specify this,
they are automatically dynamic. I only know the use for "static" in
object-oriented programming, where a static variable does not belong to an
object but to an entire class of objects. It's fine if you understand this
difference but it's fine if you don't, too, because as far as I know the Rise of
Legends scripts don't do much with object oriented programming. Here however,
only one thing is important: when some function is declared static, it can only
call other functions or variables if they are static, too. And there are
standard functions in RoL that are static, which we will see later.
...
line 18
run_once {
...
This is a special function that will only
run in the first game frame (even though it's in a callback that will run
every game frame, mind that!). So, inside the { curly brackets } belonging to
this run_once thing, you will find all the stuff that will only run once, when
the game is started. Notice how that with every { bracket opening, we enter a
deeper level! This is indicated by the amount of tabs in front of the lines.
Variables created here will not be seen on the higher levels, but we can see the
variables that were created in those higher levels. If you've forgotten, just
check the hierarchy chapter.
... line
19-28
sf_ctw_quick_battle_setup("Vinci", 6);//enable QB
setup
//sf_turn_off_all_ai();
// Get some random seeding
//ice-
don't do this - rand_seed(get_rand_seed());
disable_city_defeat("2");
disable_trigger("MyEyeNow");
disable_trigger("BoomBridge");
...
sf_ctw_quick_battle_setup
is a function that is in the "Campaigns Script Library" that was imported into
this script on line 1 (remember?). For those interested, this function enables
campaign scenarios to work about right in a quick battle type of game - in case
one ever tried. If you want to know how it works, check the "Campaigns Script
Library.bhs" file in "...\Rise of Legends\campaigns\" - it starts on line 75.
All functions that begin with "sf_" are found in that script library, by the
way.
Next, you see these weird things with "//" in front of it. "//"
means that it is a comment. The things that follow such a "//" are seen
as comments and are ignored when the script actually runs. It is to disable
things you wrote earlier without deleting them, or to explain the stuff in the
code. It's quite fun to look at the comments of BHG guys this
way.
Anyway, on line 25 the script continues with
disable_city_defeat("2"), which is apparently a void function that requires a
string parameter. Why can I see this? It is a function because of the brackets
(), and it must be a void, because if it had been a function that returned
something (in example an int, boolean or string), it should've said something
like "x = disable_city_defeat("2")", where the value the function returns is
assigned to variable x. But nay, it is just a function that is called totally on
it's own, so it must be a void. Now, this can be a function either defined in
this script itself, or defined in the standard functions of Rise of Legends. If
you look through the entire script, you won't find a definition of the function
that is called, so it must be some existing function. Now, many of those are
listed in the Script Functions Reference Sheet (downloadable somewhere), but
this one isn't, so we can only guess what it does. The "2" parameter probably
refers to player 2, meaning that when you call this function, you disable the
feature that player 2's city can be defeated, or
something.
disable_trigger("blablabla") however, on lines 27 and 28, is a
command that we can understand. It disables a trigger, and makes it do nothing
until it is activated again. We'll look at how triggers really work later on,
when we actually see some of them. For now, apparently there are triggers called
MyEyeNow and BoomBridge that need to be turned off at the start of the game.
Fine. Continue.
... lines 30-35
leader_gain_tech("2", "Territory
1");
leader_gain_tech("2", "Territory 2");
leader_gain_tech("2",
"Territory 3");
leader_gain_tech("2", "Prosperity
1");
leader_gain_tech("2", "Prosperity 2");
leader_gain_tech("2",
"Industry 1");
...
leader_gain_tech("2", "Territory 1") is not that
hard to understand, but it can be found in the reference doc as well, because
there we find:
"void leader_gain_tech(Leader the_leader, Type
the_type)"
It is a void function (doh), and it takes two parameters, one
of type Leader and one of type Type. I assume that this void instantly grants
some technology to player 2. Territory 1, 2 and 3 are probably the ones that
extend your territory's borders, and the others you can guess. These names do
not correspond simply to the names displayed in the game - you'll have to look
em up somewhere or try.
... lines
37-38
get_difficulty_data("ranconi_defense_time",
defense_spawn);
get_difficulty_data("truck_timers",
truck_move_time);
...
get_difficulty_data() is a function I cannot
find either. But I guess that it obtains the difficulty data that you created in
the scenario editor, and then stores it in the variables in the second parameter
place (defense_spawn and truck_move_time). Notice, by the way, that the
variables it's stored in have been created earlier, and they were static
(remember?). Apparently, the function get_difficulty_data() is a static
function, which can thus only work with static variables. Okidoki! Maybe what
this difficulty data really is will be more clear when we continue to go down
the script - just remember to look out for variables called "defense_spawn" and
"truck_move_time".
... lines 40-48
unit_group_move_to("entourage",
"army_start");
unit_group_patrol("patrol1",
"p1");
unit_group_patrol("patrol2", "p2");
unit_group_patrol("patrol3",
"p3");
unit_group_patrol("patrol4", "p4");
//JFR: some spideys
patrolling and stuff.
unit_group_patrol("Group Patrol Spiders",
"base_camp3")
...
Here, six more void functions are called. Let's have
a look at the descriptions from the reference sheet:
"void
unit_group_move_to (UnitGroup group, Location dest)"
This seems pretty
easy. It doesn't return anything - it's a void - and probably sends a unit group
to move to a destination. In the editor, apparently a unit group called
"entourage" was defined, and a location called "army_start". In this run_once
part (because we are still in the part that runs only once), it seems sensible
to send some army to an army starting point - why not. Next we see some called
functions of this type, again copied from the reference sheet:
"int
unit_group_patrol (UnitGroup group, Location dest)"
Now, I have no idea
how an integer function can stand on it's own, without, in example, a "x =" in
front of it. It would make more sense to me if these were void functions,
because I guess that with these functions, (in the script) five armies are being
ordered to patrol to certain points. I will update this guide once I've found
out how this works exactly. Make sure to tell me if you know! One thing I do
know is that integers often function as booleans (but with values 1 and -1
instead of true and false) - remember all the talk about variable
types?
On lines 50 to 67, more things are set and initialised. Notice the
funny comment added by BHG on lines 65 - apparently they couldn't get the stuff
on line 66 to work, so they just set the army on defensive mode hoping it would
be a sufficiently good solution. Notice the set_global()
command:
...
set_global("hide_map", 0);
...
It sets a
certain global variable called "hide_map" to zero - for some reason I don't
understand they didn't just use the variable created on line 11 (which is -
within this script - global as well). Instead there is apparently some other,
more global variable with the same name, that has to be set using set_global()
and called using get_global() - again, if anyone can explain this better then
please do so, and I will update this guide. Anyway, you will see later how this
global variable is used.
First, let's have a look at this very sweet kind
of construction: the IF!
... line 69-74
if (get_difficulty_id() ==
"Easy" || get_difficulty_id() == "Moderate")
{
remove_unit_group("patrol1");
remove_unit_group("patrol2");
remove_unit_group("patrol3");
remove_unit_group("patrol4");
}
...
The
"if" statement works fairly intuitive. It's built up like this:
if (
this condition is true ) {then do everything between these curly
brackets}.
In the script, the condition is this part: get_difficulty_id()
== "Easy" || get_difficulty_id() == "Moderate". If I tell you that the "||"
means "OR", I'm sure you can guess what this thing checks. The condition is true
when the game difficulty is easy or moderate! So, when the difficulty is easy or
moderate, it does what is between the { curly brackets }: it removes unit
groups. Apparently when the difficulty set at something not-too-hard, some
armies have to be removed from the map because otherwhise the game would be too
difficult. I think here it's time to introduce you to some logic.
Operators
Remember the boolean type, the one that
can only be set on true or false? You can actually calculate things with it,
like with numbers. We have seen it happening above, in the "if" statement, only
we did not notice. There, || meant "or", which is a so-called logical
operator. Logical operators can be seen as functions that require one or
two booleans and return a boolean. || requires two booleans as input, and
returns a new boolean. The "or" is, thank god, not the only one, here are the
important ones:
A || B is true if either A is true, or B, or if both are
true. If A and B are both false, A || B returns false.
A && B is true
only if A and B are both true. If one of them is false, the entire A &&
B is false.
!A is true only if A is not true. If A is true, !A is
false.
So, || takes two booleans are input, and returns the boolean value
true if at least one of the inputs is true, && returns true if both
inputs are true, and ! returns true when the input is not true (false). If you
want to know more about logic, I suggest you
sign up for our Age of Mythology triggers course (even if you don't have the
game Age of Mythology itself). If you don't make your scripts too complicated,
however, you hereby know enough of the logical operators.
There are
however also other kind of operators that return boolean values, what about
these:
A == B is true only if A equals B.
A < B is true only if A
is a smaller number than B.
The second one is a mathematical operator,
which can only compare numbers, like integers or floats. The == one however can
also compare two booleans, or two custom types Rise of Legends uses. Mind the
difference between "x=y" (the statement x becomes y) and "x==y" (the boolean
function that is true if x equals y). Let's zoom in on line 69 in the
script:
... line 69-74
if (get_difficulty_id() == "Easy" ||
get_difficulty_id() == "Moderate") {
...
Here the == compares a
function that returns a string, to a string, twice. First the function is
compared to the value "Easy" and then to the value "Moderate". If the first ==
returns true, OR if the second == returns true, the whole || "method" returns
true meaning that the condition is true and the thing within this if's { curly
brackets } will be done. Anyway, you have to see that these operators can for
now be seen and used as regular boolean functions. So you can
say:
...
boolean x = true;
boolean y = !x;
boolean z = x ||
y;
...
In this example, the variable called "y" will be false, but "z"
will still be true, because x is true, and that makes "x or y" true. Anyway, you
can do all kind of complicated things with the operators. Let's say A, B and C
are variables. When will the following x be true and when will it be
false?
...
boolean x = !(A && B) || (A && !(B ||
C));
...
Is x set to true when A is true but B and C aren't? Is x set
to true if A is false, B is false but C is true? See if you can figure it
out!
Back to the script
After this logical interuption, let's
go back to the script.
... line
76-81
diplo_disable("1");
diplo_disable("2");
//intro
cutscene
queue_cutscene("intro");
}
...
Apparently the diplomacy
is disabled or locked for players 1 and 2, meaning that they cannot change teams
anymore. Then, a cutscene named "intro" is started. This cutscene is created and
named in the editor, so "intro" just refers to that one. And then, what do we
see: a closing curly bracket }! It is the end of run_once, but not yet the end
of the void on_solo_game_frame(), of course.
On line 83, then, we
see:
... line 83
get_global("hide_map", hide_map);
...
So
the first thing that ever happens, in each game frame (except for the first
frame, where run_once first runs), is that the global variable hide_map is
updated. We've seen it before, and we will see it more later on. It is the
variable that keeps track of your progress through the game. Based on the value
assigned to this variable, some triggers will fire and others won't. Triggers, I
say? Yep, triggers. A fantastic feature of RoL that makes scripting much easier.
Have a look at this:
... line 88-92
trigger Startup(hide_map == 1)
{
play_music("ATBEconomic",
0);
activate_quest("river_dash");
build_group_set_seen("1",
"initially_seen");
}
...
"hide_map == 1" returns a boolean value,
and can be seen as the condition of the trigger. The effects of
the trigger, what will happen if the condition is true, are the statements
following in between the { curly brackets }. This is very similar to an if-then
construction, check:
... example
if(hide_map == 1)
{
play_music("ATBEconomic",
0);
activate_quest("river_dash");
build_group_set_seen("1",
"initially_seen");
}
...
However, working with triggers has some
great advantages over working with ifs and thens. Firstly, you can name
your triggers. The trigger starting on line 88 is called "Startup". And if
something has a name, you can refer to it! Using the enable_trigger() and
disable_trigger() statements, you can - yes - enable and disable them. If you
enable a trigger, it becomes active, meaning that it will actually run in the
next on_solo_game_frame. If it is disabled, it becomes inactive, and it'll be
ignored by the script until you enable it again. There are also shortcuts
enable_all and disable_all, that - you guessed it - enable respectively disable
all triggers at once. You aren't able to do all this with the regular ifs and
thens!
Notice that naming your triggers is optional. In example, the
trigger on line 104 doesn't have a name. Now, the rest of script until line 280
is filled with these triggers. I will not discuss any of the triggers here - it
is up to you to see what they do and how they are used. All the functions used
in those triggers can be found on the functions and callbacks reference sheet,
and often you can also just guess what they do. Just remember how javascript
works: variables, types, assignment, functions, hierarchy, etc. On line 280, we
finally see a closing curly bracket again, on the same level as the opening
bracket of the on_solo_game_frame callback. So, indeed, we're done with the
analysis of this callback, on to the next, on line
282!
"on_unit_trained()", this callback is activated whenever a unit is
trained. Four parameters will then contain the stuff like the unit ID, the
player who owns it, the type of the unit, and the building from which it was
created. Then, using a lot of if-then-else constructions, some things are
checked and stuff is done as a result. Understanding this is basically a matter
of going through it line by line. I will just explain it by splitting up and
commenting the code a bit.
... line 282-306
void game_callback
on_unit_trained(Unit the_unit, Leader the_leader, UnitType the_type, Build
the_build) {
// Create a variable "loc" that'll hold a
location.
Location loc;
// And then: if the one that created the unit
is player 2,
if (the_leader == "2") {
// If the unit was created from
barracks1 or barracks2,
// add the unit to a group,
// and send the group
to attack something.
if (the_build == "barracks1" || the_build ==
"barracks2") {
add_unit_to_group("city_defenders",
the_unit);
unit_group_attack_to("city_defenders",
"ranconi_ping");
}
// else, if it was created from the building
"bomb_factory",
// or if it was created from bomb_barracks and bridge_blown
== 0, then:
else if (the_build == "bomb_factory" || the_build ==
"bomb_barracks" && bridge_blown == 0) {
// if the unit type is a
Demo Truck, and if the break_on_through quest is active,
// and if the first
subquest is not completed, and if the break_on_through quest isn't failed,
then:
if (the_type == "Demolition Truck" &&
is_quest_active("break_on_through") &&
!is_subquest_completed("break_on_through_sub1") &&
!is_quest_failed("break_on_through")) {
// If the function
sf_blow_up_bridge returns a value bigger than 0,
// then send a chat message
(to player 1, with a voice sound, and some icons).
if (sf_blow_up_bridge()
> 0) {
add_chat_with_voice("A Demolition Truck is headed for the bridge!",
"1", "Imperial Musketeer", "Imperial Musketeer",
"vv_ranconi_imp_musk_0001");
}
// else (so if the function returns a
value equal to or smaller than 0),
// then send a different chat
message.
else {
add_chat_with_voice("A Demolition Truck is headed for the
bridge!", "1", "Imperial Musketeer", "Imperial Musketeer",
"vv_ranconi_imp_musk_0001");
}
// Anyhow, whatever that function's
value is, update the variable loc with the value of "bomb1" (created in the
editor).
loc = "bomb1";
}
// else (so if the unit type is not a
Demo Truck, or if the break_on_through quest is not active,
// or if the
first subquest is completed, or if the break_on_through quest is failed),
//
then assign "bomb2" to the variable "loc".
else {
loc =
"bomb2";
}
// After everything's done, send the unit to attack the
location stored in "loc".
unit_attack_to(the_unit, loc);
// And of
course end with enough closing brackets.
}
}
}
...
It
seems quite complicated, but the key to analysing and understanding this is to
look at the amount of tabs in front of each line. That way, you can see which
else belongs to which if, and which closing bracket belongs to which opening
bracket. Just read it through yourself - maybe it's easier to read without my
comments. On to the next callback:
... line 308
void game_callback
on_cutscene_finished (String cutscene_name, int was_skipped)
{
...
This one is activated whenever a cutscene is finished. I won't
go into this with much detail, so just check the script yourself. The script
checks the name of the cutscene that has just finished, and based on the name it
will assign a value to the global variable we've seen before - the variable
"hide_map". Remember how on each game frame, the value of this variable is
checked, and also in some triggers the value is checked (see for
yourself!).
... line 333
void game_callback
on_spell_cast(...){
...
This callback speaks for itself. It is
activated whenever a spell is called, and then if the spell "explode_truck" is
cast, the truck must be killed. Easy peasy.
The last thing we're going to
look at is the function that starts on line 339. It uses another very neat
construction - the for-loop:
... line 345-350
for (scan = 0; scan <
unit_group_length("trucks"); scan++) {
the_unit = unit_at_index("trucks",
scan);
if (is_unit_in_region(the_unit, "bridge_region"))
{
trucks_in_region = trucks_in_region + 1;
}
}
...
How does
this work? Check this:
for(initial action; check; end action;)
{actions}
The for statement first performs the initial actions, and then
continues to run as long as the check returns true. In each loop, it performs
the actions between the { curly brackets }, followed by the end action. Have
another look at the script:
... line 345
for (scan = 0; scan <
unit_group_length("trucks"); scan++) {
...
First, the variable scan is
set to 0. Then, as long as the value in scan is lower than the size of the unit
group "trucks", whatever is between the curly brackets is performed again and
again. Eventually, the value of the scan variable is increased. "scan++" is
actually a shortcut for "scan = scan +1", because it is used so often. In
summary, this thing loops through all units in the unit group, one by one.
Understood?
The end
If so, then you know enough to analyse the rest
of the script, mainly the triggers, on your own. You know everything you need to
know about variable creation and assignment, about types, about functions,
callbacks, and hierarchy. You've met the three most important constructions: the
if-then construction, the trigger, and the for-loop. You know how to calculate
with numbers and with truth values, using the mathematical respectively the
logical operators. I hope this helped, and I wish you all the best with your
scripting carreer!
Archaeopterix.
Have a guide you would like to submit to the RoLH Scenario Design Workshop? Email it to Alex. Be sure to include your forum username and the guide's title.
Note - Only the best guides will be added here. Please keep your guides clear and to the point. Feel free to post your guide in the Scenario Design forums at Rise of Legends Heaven where your guide will be looked over by the staff and may be added to the Workshop.











