Tomb Raider Forums  

Go Back   Tomb Raider Forums > Tomb Raider Level Editor and Modding > Tomb Raider Level Editor > Tutorials and Resources

Closed Thread
 
Thread Tools
Old 09-09-24, 19:23   #1
AkyV
Moderator
 
Joined: Dec 2011
Posts: 5,074
Default TEN - Script elements and operations (advanced)

Made using Tomb Editor 1.7.1. pack (including Tomb Engine 1.4.)
Last update to TE/TEN: 1.7.2./1.5.


In the tutorial about the script files I started a tutorial sequence about TEN scripting techniques.

The further parts of the sequence so far:
functions, basic scripting.

So this current tutorial is the fourth part about scripting techniques in TEN. It is about the advanced script elements and operations.

One day I'll probably extend this tutorial with some further LUA features. But I just don't know enough about advanced LUA scripting, to do that now.
However, after this tutorial don't wait for a "TEN - Script elements and operations (expert)" tutorial to be released any time soon. I will surely not be clever enough to write something like that, nowadays or in the near future.
So the tutorial sequence will continue with something else...


CONTENTS:

1. Tables
2. Loops
3. Multiple assignments
4. Advanced mathematical operations
5. Bitwise operations
6. String operations

----------

Notes:
  • As the title says, it is “only” a Tomb Engine (TEN) tutorial. Many aspects you need to know for this tutorial are specific Tomb Editor (TE), TombIDE (TIDE), WadTool (WT) etc. features.
    So non-TEN features are only referred now, if it is necessary. And as much as I found it necessary. You need to also meet these features in other tutorials, for further details about them.
  • Some related TEN features are only mentioned, but not discussed in this tutorial, because they belong better to (an)other tutorial(s).
    Find another info source or be patient till those other tutorials are made.
  • Some options or commands can be executed in several ways. I may not introduce all of the ways in the tutorial. (Using default keys are presumed.)
  • Explainig basics about Lua scripting is not the best decision in a precise way when you, the reader, is a beginner TEN builder. That is why I try to interpret Lua scripting mostly with my own words - while I also try to remain authentical, naturally. (Which is not too hard, anyway, because when I am making this tutorial sequence then I am not a Lua expert myself...)
  • Perhaps you understand the Lua basics better in a form of another tutorial.
    Help dropdown menu of Scripting Studio also leads you to this linked tutorial, anyway.
  • You can't find OnUseItem callback in the examples of the tutorial, because those are made with a previous version of TE.

Last edited by AkyV; 04-11-24 at 20:23.
AkyV is online now  
Old 09-09-24, 19:37   #2
AkyV
Moderator
 
Joined: Dec 2011
Posts: 5,074
Default

1. Tables

In this tutorial sequence we previously mentioned "tables": TEN master table has sub-tables ("modules") which has sub-subtables ("classes"), and they can also have functions and constants.
Yes, a purpose of a table is to store multiple values at the same time - unlike variables which can store only one value at the same time.

If you are an advanced or expert TRNG builder, then tables should be familiar to you.
See for example this script command, whose purpose is to store big numbers:

Parameters = PARAM_BIG_NUMBERS, 1286, -31713, 18565, -771, 5654, 397

So, for example, when you want to use the number of 18565, then you will call a trigger (or another script command) which refers to the 3rd position of PARAM_BIG_NUMBERS "table".

The important things you initially need to know about TEN tables are:
  • They can store only the same data type at the same time. (I.e. eg. if a table has texts, then you cannot store even numbers in it. - Technically it is available, but mostly seems meaningless.)
  • The table values are separated by commas.
  • They can be either global or (labelled as) local, just like the variables.
  • Also just like at the variables: they don't exist initially, feel free to create any table, with any (but a thoughtful) name, not worrying about the table size.
  • Your eyes will easily discriminate variables from tables, if you search for {} brackets, following the table name and the equals sign:

    local RoomID = 6

    This is RoomID local variable, storing seemingly a room ID.

    local RoomID = {6, 12, 7}

    And this is RoomID local table, storing more than one room ID. (I don't recommend to have the similar name for variables and tables at the same time, though.) If you want to use eg. the second number (12) of the table, then you will need a command later, which will call that second position.

    local NatlasTeam = {"Natla", "Pierre", "Larson", "Kold", "Kid", "Cowboy"}

    And this is an example for a textual table.
Indices

As I said above, we need to identify a position of a value in a table, so we will able to call that table value.
The method for this is we use our tables in the form of arrays, i.e. using indices for the values.

If you don't want any specific index (I will tell below how you can), then the first value of the table will always have ID 1, the second value of the table will always have ID 2, the third value of the table will always have ID 3 etc. (So this is what I wall call "regular indices".)
The required index always needs to be typed in [] brackets.
A table value can be called if you name the table, with the required index together.

For example now "Pierre" will be printed on the screen:

Code:
local NatlasTeam = {"Natla", "Pierre", "Larson", "Kold", "Kid", "Cowboy"}
printMember = DisplayString(NatlasTeam[2], 250, 250, Color(255,0,0))
ShowString(printMember, 10)
As you can see, the position of a table technically works like a variable.
That is why you can set a table value even as a variable value:

Code:
local A ="Natla"
local NatlasTeam = {A, "Pierre", "Larson", "Kold", "Kid", "Cowboy"}
Or, a table value could be even a function result (because, as you know, function results also can be copied into variables):

Code:
local A = TEN.Objects.GetMoveableByName("Natla")
local NatlasTeam = {A:GetName(), "Pierre", "Larson", "Kold", "Kid", "Cowboy"}
Inserting/removing values

You don't need to create a table with values everyway. I mean, you are allowed to create an empty table, which will be stuffed only later with values - all you naturally need for an empty table is to use empty brackets:

local NatlasTeam = {}

Now I show the method how to fill the empty table with values, using regular indices:
When you insert a value in an empty table (with a table.insert initial LUA function), that will surely go into the first table position (i.e. index = 1), which is now the first empty table position. The next value inserted in that table will surely go into the second table position (index = 2), which is now the first empty table position, etc.
For example it makes the same table, as above:

Code:
local NatlasTeam = {}
table.insert(NatlasTeam, "Natla")
table.insert(NatlasTeam, "Pierre")
table.insert(NatlasTeam, "Larson")
table.insert(NatlasTeam, "Kold")
table.insert(NatlasTeam, "Kid")
table.insert(NatlasTeam, "Cowboy")
Or naturally eg. it also has the same result:

Code:
local NatlasTeam = {"Natla", "Pierre", "Larson"}
table.insert(NatlasTeam, "Kold")
table.insert(NatlasTeam, "Kid")
table.insert(NatlasTeam, "Cowboy")
If you want to insert a value in an existing position, then you also need to type the index of this position:

table.insert(NatlasTeam, 5, "Winston")

But it doesn't overwrite the previous value of that position. Instead, it pushes away that by one position.
So now "Kid" automatically will be pushed away from ID5 to ID6. And that is why "Cowboy" automatically will be pushed away from ID6 to ID7.
But if you want to overwrite the previous value, then first remove it (with a table.remove initial LUA function), and use the inserting function only after that, for that position:

table.remove(NatlasTeams, 5)
table.insert(NatlasTeam, 5, "Winston")


Now first "Kid" will be removed from ID5, and "Cowboy" will be pulled back from ID6 into the empty ID5.
And then "Winston" will be inserted in ID5, pushing "Cowboy" back into ID6.

There is another way to add a value to an empty table position - but this time you always need to name exactly the index which that value is inserted into.
On the other hand, this time you don't need to add the values in the order of indices - so this time eg. you will still insert "Cowboy" into ID6, even if you added "Kid" in the 6th line:

Code:
local NatlasTeam = {}
NatlasTeam[1] = "Natla"
NatlasTeam[2] = "Pierre"
NatlasTeam[3] = "Larson"
NatlasTeam[6] = "Cowboy"
NatlasTeam[4] = "Kold"
NatlasTeam[5] = "Kid"
printMember = DisplayString(NatlasTeam[6], 250, 250, Color(255,0,0))
ShowString(printMember, 10)
So it looks technically the same when you set a value for a variable.

This time referring to an existing index won't push the other values away, but simply overwrites the value in that index:

NatlasTeam[5] = "Winston"

So this time "Kid" was overwritten at ID5, that index already belongs to "Winston". "Cowboy" won't leave ID6, it remains always there.

But you can also use this "another" method as a way to set irregular indices, which means:
  • You can place "holes" in the index list, so the indices will be irregular, instead of being regular (1, 2, 3, 4 etc.):

    Code:
    local NatlasTeam = {}
    NatlasTeam[7] = "Natla"
    NatlasTeam[24] = "Pierre"
    NatlasTeam[58] = "Larson"
    NatlasTeam[273] = "Cowboy"
    NatlasTeam[79] = "Kold"
    NatlasTeam[108] = "Kid"
    printMember = DisplayString(NatlasTeam[273], 250, 250, Color(255,0,0))
    ShowString(printMember, 10)
  • You can use variables to set an index (irregular ones, just like in this example, or even regular ones: 1, 2, 3, 4 etc.):

    Code:
    local NatlasTeam = {}
    local A = 7
    local B = 24
    local C = 58
    local D = 273
    local E = 79
    local F = 108
    NatlasTeam[A] = "Natla"
    NatlasTeam[B] = "Pierre"
    NatlasTeam[C] = "Larson"
    NatlasTeam[D] = "Cowboy"
    NatlasTeam[E] = "Kold"
    NatlasTeam[F] = "Kid"
    printMember = DisplayString(NatlasTeam[D], 250, 250, Color(255,0,0))
    ShowString(printMember, 10)
  • You can use even textual indices (which are naturally also irregular indices):

    Code:
    local NatlasTeam = {}
    NatlasTeam["Boss"] = "Natla"
    NatlasTeam["Thug1"] = "Pierre"
    NatlasTeam["Thug2"] = "Larson"
    printMember = DisplayString(NatlasTeam["Thug1"], 250, 250, Color(255,0,0))
    ShowString(printMember, 10)
  • Deleting a value from the table, without pulling back the values of the following indices (i.e. leaving a "hole" in the index list here):

    NatlasTeam[5] = nil
The last index

You can also use # sign, which tells the length of a table.
It could be important eg. if your table is really long (so eg. you can't remember/you don't want to count what the index of the last value of the table is), and you want to insert a new value in the last but one position of the "longtable" numerical table:

longtable[#longtable - 1] = 125

Yes, "#longtable" means "the index of the last value of longtable table".

But it works nicely only if you used the regular way (1, 2, 3, 4 etc.) to set indices for this table.

A table of tables

As you could see above, eg. the parts of TEN master table (i.e. the modules) are other tables. I.e. you could make even "a table of tables" - eg. splitting "NatlasTeam" table into two subtables:

Code:
local NatlasTeam = {{"Natla", "Pierre", "Larson"}, {"Kold", "Kid", "Cowboy"}}
printMember = DisplayString(NatlasTeam[2][3], 250, 250, Color(255,0,0))
ShowString(printMember, 10)
Or perhaps it looks better this way:

Code:
local NatlasTeam = {
     {"Natla", "Pierre", "Larson"},
     {"Kold", "Kid", "Cowboy"}
}
printMember = DisplayString(NatlasTeam[2][3], 250, 250, Color(255,0,0))
ShowString(printMember, 10)
So you created "NatlasTeam" table, regularly having two values even at the creation:
  • At ID1 = subtable of {"Natla", "Pierre", "Larson"}
  • At ID2 = subtable of {"Kold", "Kid", "Cowboy"}
As you can see, the subtables also got their values when they were created, that is why their indices are also regular. So eg. in the case of the second subtable, the indices inside the subtable are: ID1 = Kold, ID2 = Kid, ID3 = Cowboy.
So in the case of that position in the printing function (NatlasTeam[2][3]):
  • The first [] always refers to the index in the main table, which is 2 this time, which means the second subtable of "NatlasTeam".
  • The second [] always refers to the index in the subtable, which is 3 this time, which means ID3 = Cowboy.
So NatlasTeam[2][3] says now that the game should print "Cowboy" on the screen.

Naturally now you need the same methods to insert/remove values, as I said above:

Code:
local NatlasTeam = {}
table.insert (NatlasTeam, {"Natla", "Pierre", "Larson"})
table.insert (NatlasTeam, {"Kold", "Kid", "Cowboy"})
Code:
local NatlasTeam = {}
NatlasTeam[1] = {"Natla", "Pierre", "Larson"}
NatlasTeam[2] = {"Kold", "Kid", "Cowboy"}
(Once we declared that NatlasTeam is local, so we don't need to declare it again for the subtables.)

Code:
local NatlasTeam = {}
NatlasTeam["Team1"] = {"Natla", "Pierre", "Larson"}
NatlasTeam["Team2"] = {"Kold"}
NatlasTeam["Team2"][2] = "Kid"
NatlasTeam["Team2"][3] = "Cowboy"
printMember = DisplayString(NatlasTeam["Team2"][3], 250, 250, Color(255,0,0))
ShowString(printMember, 10)
And here it is a mixed version (i.e. when some indices of the table refer to simple values, not subtables) - now the subtable is at ID4:

Code:
local NatlasTeam = {"Natla", "Pierre", "Larson", {"Kold", "Kid", "Cowboy"}}
printMember = DisplayString(NatlasTeam[4][3], 250, 250, Color(255,0,0))
ShowString(printMember, 10)
And you can try even further levels for tables, if you want. - Eg. three levels ("a table of tables of tables") in this example:

Enemies["animals"]["big ones"][5]

Further functions for tables

Further initial LUA functions to handle tables (just like at other "table..." functions, recommended to use them only with regular indices):
  • table.concat:
    This function always works with texts (so when the table values are numbers, you don't need tostring function, the conversion into texts is automatical).
    The function is useful eg. if you want to print on the screen not only a value of the table, but more of its values. Perhaps all the table values.
    The first (and only mandatory) argument in the function is the table name:

    Code:
    local NatlasTeam = {"Natla", "Pierre", "Larson", "Kold", "Kid", "Cowboy"}
    printTable = DisplayString(table.concat(NatlasTeam), 250, 250, Color(255,0,0))
    ShowString(printTable, 10)
    Now the whole contents of the table will be printed (in their order) - without spaces between the values:

    NatlaPierreLarsonKoldKidCowboy

    You also need a second (optional) argument, as a separator between the printed values:

    table.concat(NatlasTeam, ", ")

    Natla, Pierre, Larson, Kold, Kid, Cowboy

    A third (optional) argument sets a starting point (an index, without []), if you want to start this printing after the first index of the table:

    table.concat(NatlasTeam, ", ", 2)

    Pierre, Larson, Kold, Kid, Cowboy

    A fourth (optional) argument sets an ending point (an index, without []), if you want to finish this printing before the last index of the table:

    table.concat(NatlasTeam, ", ", 2, 4)

    Pierre, Larson, Kold

    Naturally type 1 for the starting point, if you'd like to change only the ending point.
  • table.move:
    This function copies/pastes a part of a table into another table:

    Code:
    local NatlasTeam = {"Natla", "Pierre", "Larson"}
    local ToNatla = {"Kold", "Kid", "Cowboy"}
    table.move(ToNatla, 1, #ToNatla, #NatlasTeam + 1, NatlasTeam)
    printTable = DisplayString(table.concat(NatlasTeam, ", "), 250, 250, Color(255,0,0))
    ShowString(printTable, 10)
    Now what you copy is the whole "ToNatla" table. The whole, because the starting point of the copy is at ID1, and the ending point of the copy is at the last index (#ToNatla). The table values will be pasted (in their original order) into "NatlasTeam" table, just after its last position (#NatlasTeam).
    That is why this is what you will print on the screen now:

    Natla, Pierre, Larson, Kold, Kid, Cowboy

    If the pasting point means an existing point of "NatlasTeam", then the value there (and the following values in greater indices) will be overwritten:

    table.move(ToNatla, 1, #ToNatla, #NatlasTeam - 1, NatlasTeam)

    "#NatlasTeam - 1" is NatlasTeam[2], so "Pierre" will be overwritten there by "Kold". "Kid" also overwrites "Larson" in NatlasTeam[3]. "Cowboy" gets the empty NatlasTeam[4] index:

    Natla, Kold, Kid, Cowboy

    But naturally you are allowed to copy even only a part of a table:

    table.move(ToNatla, 1, 2, #NatlasTeam - 1, NatlasTeam)

    Natla, Kold, Kid

    And if you don't set the fifth argument as the destination table, then the copied part will be pasted into the source table itself:

    Code:
    table.move(ToNatla, 1, 3, 4)
    printTable = DisplayString(table.concat(ToNatla, ", "), 250, 250, Color(255,0,0))
    ShowString(printTable, 10)
    Kold, Kid, Cowboy, Kold, Kid, Cowboy

  • table.pack:
    This function is simply another method to create a table with the regular indices.
    So these two lines do the same:

    local NatlasTeam = {"Natla", "Pierre", "Larson", "Kold", "Kid", "Cowboy"}

    local NatlasTeam = table.pack("Natla", "Pierre", "Larson", "Kold", "Kid", "Cowboy")

  • table.sort:
    This function arranges the values of a table into an ascending order:

    Code:
    local numbers = {9, 143, 52, 65, 48}
    table.sort(numbers)
    printTable = DisplayString(table.concat(numbers, ", "), 250, 250, Color(255,0,0))
    ShowString(printTable, 10)
    And now what you will see on the screen is:

    9, 48, 52, 65, 143

    If you want a different order, then you need an optional second argument, which is the "yes or no" output of a custom function:

    Code:
    function Descending (a, b)
    	return a > b
    end
    
    local numbers = {9, 143, 52, 65, 48}
    table.sort(numbers, Descending)
    printTable = DisplayString(table.concat(numbers, ", "), 250, 250, Color(255,0,0))
    ShowString(printTable, 10)
    It doesn't matter that what's the name of the custom function or its arguments.
    What matters is the function should have a return value, where there is the required relation between the two arguments.
    So now it will be printed on the screen:

    143, 65, 52, 48, 9

  • table.unpack:
    This function turns the values of a table into variables (not hurting the table itself):

    Code:
    local NatlasTeam = {"Natla", "Pierre", "Larson"}
    local A, B, C = table.unpack(NatlasTeam)
    So the first table value will be the first variable (A) value, the second table value will be the second variable (B) value etc. - That is why now:
    A value is "Natla".
    B value is "Pierre".
    C value is "Larson".

    Or they can be turned even into arguments:

    Code:
    function Math (A, B, C)
    	D = (A + B) * C
    end
    local functionInputs = {6, 21, 8}
    Math(table.unpack(functionInputs))
    So the first table value will be the first argument (A) value, the second table value will be the second argument (B) value etc. - That is why now:
    A value is 6.
    B value is 21.
    C value is 8.
    D = (6 + 21) * 8 = 216.
----------

Notes:
  • Leaving a comma after the last value is meaningless, but not an error. It won't change even the last index value:

    local NatlasTeam = {"Natla", "Pierre", "Larson", "Kold", "Kid", "Cowboy", }

  • Semicolon is just an informal separation between two parts of the list, technically it exactly works like a comma:

    local NatlasTeam = {"Natla", "Pierre", "Larson"; "Kold", "Kid", "Cowboy"}

  • Irregular numerical indices can be set even when the table is created, still typed in [] brackets:

    local NatlasTeam = {[-164] = "Natla", [82] = "Pierre", [26] = "Larson"}

    Yes, there can be even negative numerical indices.

    If the first index is 0 now, then you don't need to set the further indices. In this case the further indices will be 1, 2, 3 etc. automatically, in the order of the table values:

    local NatlasTeam = {[0] = "Natla", "Pierre", "Larson"}

    So Pierre has ID1, Larson has ID2 now.
  • Textual indices can be set even when the table is created. Now you need to do that without [] or "" for the indices (so they look like variable names now, but they are not):

    local NatlasTeam = {Boss = "Natla", Thug1 = "Pierre", Thug2 = "Larson"}

    Besides, if you don't want to print [] and "" when you call a textual index, then optionally simply use a dot instead of those:

    NatlasTeam.Thug1

    And it is very interesting, because that means that our previuos "Team1"/"Team2" example can be typed even in this way:

    Code:
    local NatlasTeam = {}
    NatlasTeam.Team1 = {"Natla", "Pierre", "Larson"}
    NatlasTeam.Team2 = {"Kold"}
    NatlasTeam.Team2[2] = "Kid"
    NatlasTeam.Team2[3] = "Cowboy"
    printMember = DisplayString(NatlasTeam.Team2[3], 250, 250, Color(255,0,0))
    ShowString(printMember, 10)
    And why is it interesting? Because it is the same groupping method I introduced previously (see the links in the first post) for functions and variables (yes, it means technically those groups are also tables):

    local NatlasTeam = {}
    NatlasTeam.Team1 =


    LevelVars.object = {}
    LevelVars.object.objectSize =


    LevelFuncs.PrintAnything = {}
    function LevelFuncs.PrintAnything.PrintHello()


    So, the method with [] and "" is alternative even in their cases:

    LevelVars["object"] = {}
    LevelVars["object"]["objectSize"] =


    LevelFuncs["PrintAnything"] = {}
    LevelFuncs["PrintAnything"]["PrintHello"] = function()


    (Yes, perhaps it is important to use "=function()" form to define a function, when you use [] and "".)
  • As I said, usually we should use regular indices for a proper last index, so we can bravely say now that the last index is the amount of the elements in the table.
    Eg. we have these indices: 1, 2, 3, 4, 5, 6. The last index is 6, and the amount of the values is also 6.
  • In table.sort function that second argument for a custom order could be even more complex - mostly when the table is textual, and you want to sort the values in some alphabetical order.
    But we won't discuss it in this tutorial.
  • Table.unpack is useless with tables having irregular indices. But in those cases you can use a manual way to turn the table values into variables:

    local NatlasTeam = {[-164] = "Natla", [82] = "Pierre", [26] = "Larson"}
    local A = NatlasTeam[-164]
    local B = NatlasTeam[82]
    local C = NatlasTeam[26]


  • As you can see, technically tables work like variables. - That is why:

    • Don't forget what I told in that basic scripting tutorial about identified things - eg. objects - not being numbers or texts to print them as variable values. So we can tell the same for table values.
    • If you want to keep the table values when a savegame is loaded or transferred into another level, then you need to use them as LevelVars or GameVars tables, like:

      Code:
      local LevelVars.NatlasTeam = {"Natla", "Pierre", "Larson", "Kold", "Kid", "Cowboy"}
      printMember = DisplayString(LevelVars.NatlasTeam[2], 250, 250, Color(255,0,0))
      ShowString(printMember, 10)
      Don't forget what I told in that basic scripting tutorial about LevelVars/GameVars having bugs and limitations.

  • Check "next" initial LUA function (with the table name as the argument) for nil. If the answer is yes (true), then the table is empty:

    if next(table_name) == nil then

Last edited by AkyV; 08-11-24 at 17:45.
AkyV is online now  
Old 10-09-24, 09:56   #3
AkyV
Moderator
 
Joined: Dec 2011
Posts: 5,074
Default

2. Loops

Previously we talked about the OnLoop game phase which is running all through the effective playtime of your current level (except when menus are open). So whatever you type in this phase of the script, that will be called again and again, once in each moment of this playtime.
Naturally usually you do not run (call) anything during all the playtime. Conditions helps you to narrow it down. Eg. if the condition is "if Lara is holding pistols", then that game event (eg. to spawn an enemy) will happen only when Lara has pistols in the hands. And if you don't want to call that in each moment when Lara is holding pistols, then you need further conditions to narrow it more down: "if Lara is holding pistols and if...". - Yes, it is a typical TRNG GlobalTrigger logic.
And if it is still not enough to you, then you have further tools to control more these "TEN globaltriggers": control them by some timers, or enable/disable them in specific moments. - But things like that will be discussed in a latter tutorial, those are not parts of the scripting techniques tutorial sequence (because you will use techniques for them, which you have already learnt in the meanwhile).

I say that because you need to understand: when I am talking about looped operations (i.e. loops), that is something completely different, it has nothing to do with OnLoop logic.
I mean loops are initial LUA operations. In their case the loop doesn't mean "many moments after each other". In their case loop means "many happenings after each other, in the same moment". (Which means if a loop is typed in OnLoop then you also need conditions and such, to tell the moment when the loop will be running. Otherwise the whole loop will fully run once in each moment of OnLoop: once fully in the first moment, then once fully in the second moment, then once fully in the third moment etc.)

Loops are useful when you want to script numerous similar happenings, which are for the same purpose, but each of these happenings have one or more different parameters. I.e. in this case you can solve it by typing one line in the script, for each happening - or you won't type numerous lines, instead you type only a few lines for a loop.

Let's see for example when you want to print "Hello, Lara!" six times, in the moment when the level starts, printing it in six different positions: one of the coordinates is constant, it is 250 pixels from the left edge of the screen. What is different is the other coordinate: it is 250, 350, 450, 550, 650, 750 pixels from the upper edge of the screen:

Click image for larger version

Name:	97.jpg
Views:	26
Size:	91.6 KB
ID:	7465

Without loops, it works only with some tiresome, long scripting:

Code:
LevelFuncs.OnStart = function()
	local printLara1 = DisplayString("Hello, Lara!", 250, 250, Color(255,0,0))
	ShowString(printLara1, 10)
	local printLara2 = DisplayString("Hello, Lara!", 250, 350, Color(255,0,0))
	ShowString(printLara2, 10)
	local printLara3 = DisplayString("Hello, Lara!", 250, 450, Color(255,0,0))
	ShowString(printLara3, 10)
	local printLara4 = DisplayString("Hello, Lara!", 250, 550, Color(255,0,0))
	ShowString(printLara4, 10)
	local printLara5 = DisplayString("Hello, Lara!", 250, 650, Color(255,0,0))
	ShowString(printLara5, 10)
	local printLara6 = DisplayString("Hello, Lara!", 250, 750, Color(255,0,0))
	ShowString(printLara6, 10)	
end
Or, without variables, it is a bit less scripting:

Code:
LevelFuncs.OnStart = function()
	ShowString(DisplayString("Hello, Lara!", 250, 250, Color(255,0,0)), 10)
	ShowString(DisplayString("Hello, Lara!", 250, 350, Color(255,0,0)), 10)
	ShowString(DisplayString("Hello, Lara!", 250, 450, Color(255,0,0)), 10)
	ShowString(DisplayString("Hello, Lara!", 250, 550, Color(255,0,0)), 10)
	ShowString(DisplayString("Hello, Lara!", 250, 650, Color(255,0,0)), 10)
	ShowString(DisplayString("Hello, Lara!", 250, 750, Color(255,0,0)), 10)	
end
But it will be really comfortable with a loop - see below.

"For" loops

"For" loops are probably the loops which are used most of the time.
Our previous example should be typed with a "for" loop in a way like this:

Code:
LevelFuncs.OnStart = function()
	for i = 0, 500, 100 do
		local printLara = DisplayString("Hello, Lara!", 250, 250 + i, Color(255,0,0))
		ShowString(printLara, 10)
	end
end
As you can see, loops work as blocks, which is why what you type between "for" and "end", that is what "for" loop should execute in that moment: the loop is reading the lines one after the other, between "for" and "end", during its first running. When it reaches "end", it jumps back to "for", to read the lines again, during its second running. Etc.
When you define the purpose of a loop, then it always starts with defining different values for a variable. Yes, this time 0, 500 and 100 are all to define different values, for the same variable. Usually we name this variable of loops always "i" (but it is not a must at all), and it is always local for this block, even if it is not declared.
After the "do" tag you should type the functions (or other operations) what this loop will do. Your task is to use that variable value everyway in these functions. As you can see, this time what we do is adding the variable value to the coordinate of the text, which coordinate is compared to the upper edge of the screen.
The three values for i variable are:
  • The first value (0) is the starting value of the variable: this is the value of the variable at the first running of the loop.
  • The second value (500) is the ending value of the variable: this is the value of the variable at the last running of the loop. When the loop reaches "end" during this running, then it won't run again any more, the loop won't jump back to "for". Instead, the game starts reading the further script lines, after this "end".
  • The third value (100) tells that how much the variable value will change between two runnings: i.e. if the starting value is 0, then the value at the second running will be 100, at the third running will be 200 etc.
    This third value is optional. If you miss it (including the comma in front of it), then the default 1 value will be used.
Which means the variable will have six values while the loop is running, in this order: 0, 100, 200, 300, 400, 500.
Seeing that "250 + i" addition, that value will be 250 + 0 = 250 first, then 250 + 100 = 350, then 250 + 200 = 450 etc. So these will be the values of that coordinate: 250 at the first running, 350 at the second running, 450 at the third running, 550 at the fourth running, 650 at the fifth running, 750 at the sixth (last) running.
So the contents of the loop will be executed six times in the moment when the level starts, printing "Hello, Lara!" text on (250, 250), (250, 350), (250, 450), (250, 550), (250, 650) and (250, 750) positions of the screen.

Some examples for "for" loops and tables:
  • You can use the loop to fill an empty table with values:

    Code:
    local a = 0
    local tableValues = {}
    for i = -10, 10, 2 do
            a = a + 1
    	tableValues[i] = a
    end
    Variable i this time eventually also means the current index of the table. Seemingly these are irregular indices now (-10, -8, -6 etc.), not regular ones (1, 2, 3 etc.). Before the loop starts, you define variable a as 0. Variable a is the current value of the table, so the table values will be 1, 2, 3 etc., because each value is increased by 1, compared to the previous value of a. (I mean, see a = a + 1, so first it is 0 + 1 = 1, then it is 1 + 1 = 2, then it is 2 + 1 = 3 etc.)
    So the indices and their values are:

    [-10] = 1
    [-8] = 2
    [-6] = 3
    [-4] = 4
    [-2] = 5
    [0] = 6
    [2] = 7
    [4] = 8
    [6] = 9
    [8] = 10
    [10] = 11


    Three notes:

    • The table is declared before the loop, because you naturally don't need to declare that empty table again and again, at each i value of the loop.
    • I experienced bad test results when a variable, labelled as local, refreshed its value from its previous value. Just like now: a = a + 1. It is true even if you want to use this value only in the same block (tableValues[i] = a).
      That is why I suggest using these situations always as global variables (just as I did now), even if you don't want to use that value out of that block.
    • You cannot define a = 0 in the loop, because in that case the loop turns a to 0 at each i, so all the table values will be 1. - I.e. it is a bad scripting:

      Code:
      local tableValues = {}
      for i = -10, 10, 2 do
              local a = 0
              a = a + 1
      	tableValues[i] = a
      end
  • A similar example when you fill a table of tables with values (yes if there is another loop in the loop, then the variable of the "inner loop" is usually called "j", not "i").
    "For i" declares five empty subtables, and "for j" declares three values for each subtable (seemingly the full "for j" loop is called once for each i value):

    Code:
    local tableValues = {}
    local a = 0
    for i = 1, 5 do
    	tableValues[i] = {}
    	for j = 1, 3 do
    		a = a + 1
     		tableValues[i][j] = a
    	end
    end
    So the indices and their values are:

    [1] [1] = 1
    [1] [2] = 2
    [1] [3] = 3
    [2] [1] = 4
    [2] [2] = 5
    [2] [3] = 6
    [3] [1] = 7
    [3] [2] = 8
    [3] [3] = 9
    [4] [1] = 10
    [4] [2] = 11
    [4] [3] = 12
    [5] [1] = 13
    [5] [2] = 14
    [5] [3] = 15


  • You have ten flames together in a little area of a dark room. Lara will ignite them one by one, with triggers placed in the map. The orangish light color of the flames will lighten the room walls. But it is not enough to you this time: you want a bigger radius for the orangish light.
    No, this radius is not a customizable value at flames. But you can do some workaround: you will put a light bulb in the middle of that area, adjusted for RGB = 200/100/20 orangish color, having a huge action radius. - But there are two problems with that:

    • The light bulb must be turned off when the flames are off, and must be turned on when the flames are on.
    • The light bulb should have different light intensities: lower, when only a few flames are on, and higher, when more flames are on.

    You can use a flipmap to turn on/off the bulb, but that is seemingly not enough now.
    That is why you will use a unique TEN feature now: a function (EmitLight) which is able to reproduce the effect of a light bulb. So instead of placing a light bulb in that position of a map, you will run this script function.
    One of the function arguments now is about map coordinates - so you type these coordinates for the function, where you originally wanted to place that bulb.
    Another function argument is naturally the light color: so you will have a script condition to tell the game that the orangish color must be darker when only a few flames are on, and must be lighter, when more flames are on.
    The script you will use now is:

    Code:
    -- FILE: Levels\My_level.lua
    
    function FlameAmbience()
        local flameOn = 0  
        for i = 1, 10 do
           	if GetMoveableByName(LevelVars.Flames[i]):GetActive() == true then
               	flameOn = flameOn + 1
    	end    	
        end
        EmitLight(Vec3(12288, -1280, 10086),
        	Color(200 * flameOn / 10, 100 * flameOn / 10, 20 * flameOn / 10), 20)    	
    end
    
    LevelFuncs.OnLoad = function() end
    LevelFuncs.OnSave = function() end
    LevelFuncs.OnStart = function()
    	LevelVars.Flames = {}
    	for i = 1, 10 do 
    		LevelVars.Flames[i] =  "flame_emitter_" .. i
    	end
    end
    LevelFuncs.OnLoop = function()
    	FlameAmbience()
    end
    LevelFuncs.OnEnd = function() end
    So, first of all, you have five FLAME_EMITTER objects placed in the room, with these names: flame_emitter_1, flame_emitter_2, ... flame_emitter_9, flame_emitter_10. (If the names are not these, then naturally you can rename them in TE.)
    When the level starts, then you make a LevelVars.Flames table, filling that with these names (LevelVars is important now, otherwise the game will forget to restore the table values in loaded savegames):

    [1] = "flame_emitter_1"
    [2] = "flame_emitter_2"
    [3] = "flame_emitter_3"
    [4] = "flame_emitter_4"
    [5] = "flame_emitter_5"
    [6] = "flame_emitter_6"
    [7] = "flame_emitter_7"
    [8] = "flame_emitter_8"
    [9] = "flame_emitter_9"
    [10] = "flame_emitter_10"


    Then the game will call FlameAmbience() function in each moment once, in the game loop:
    First it will declare that flameOn variable value will always be 0 initially in the current moment.
    Then it will run a "for" loop. The loop will call each flame name stored in LevelVars.Flames table, to check if the flame of that name is already ignited or not. (I.e. if GetActive() is true or not.) If that is, then flameOn value of the previous check (or the basic 0 value if there hasn't been a check yet) will be increased by 1.- For example, in a random moment of the game loop:

    In that moment first it checks flame_emitter_1. It is still not burning, so flameOn remains 0. Then it checks flame_emitter_2. It is already burning, so flameOn turns into 0+1=1. Then it checks flame_emitter_3. It is already burning, so flameOn turns into 1+1=2. Then it checks flame_emitter_4. It is still not burning, so flameOn remains 2. Then it checks flame_emitter_5. It is already burning, so flameOn turns into 2+1=3. Etc. Let's suppose flameOn value will be 6 when the loop ends, i.e. the value of flameOn in the current moment is 6.

    The next thing that the function will do is calling EmitLight function (typed in two rows, due to the long argument list) with specific intensity values, for the current moment:

    If flameOn of that moment is 0, then the room is still fully dark, the EmitLight won't emit any orangish light. (0 % of full intensity. Because 0 flame, i.e. 0 % of the flames is ignited now.)
    If flameOn of that moment is 1, then the room is a bit lighter, the EmitLight will emit a very weak orangish light. (10 % of full intensity. Because 1 flame, i.e. 10 % of the flames is ignited now.)
    If flameOn of that moment is 2, then the room is a bit more lighter, the EmitLight will emit a not too weak orangish light. (20 % of full intensity. Because 2 flames, i.e. 20 % of the flames are ignited now.)
    ...
    If flameOn of that moment is 9, then the room is really shiny, the EmitLight will emit almost the full light intensity. (90 % of full intensity. Because 9 flames, i.e. 90 % of the flames are ignited now.)
    If flameOn of that moment is 10, then the room is full shiny, the EmitLight will emit the full light intensity. (100 % of full intensity. Because 10 flames, i.e. 100 % of the flames are ignited now.)

    flameOn=0, RGB=0/0/0
    flameOn=1, RGB=20/10/2
    flameOn=2, RGB=40/20/4
    flameOn=3, RGB=60/30/6
    flameOn=4, RGB=80/40/8
    flameOn=5, RGB=100/50/10
    flameOn=6, RGB=120/60/12
    flameOn=7, RGB=140/70/14
    flameOn=8, RGB=160/80/16
    flameOn=9, RGB=180/90/18
    flameOn=10, RGB=200/100/20


    Why? Let's see for example the red light component. (But naturally we could tell similar things for the green or the blue light components.)
    The constant value here typed is 200. It is the maximum, i.e 100 % of the red component of the light. But it is always multiplied by flameOn/10.
    So if flameOn=0, then red=200*0/10=the 0 % of 200=0.
    If flameOn=1, then red=200*1/10=the 10 % of 200=20.
    If flameOn=2, then red=200*2/10=the 20 % of 200=40.
    ...
    If flameOn=9, then red=200*9/10=the 90 % of 200=180.
    If flameOn=10, then red=200*10/10=the 100 % of 200=200.

    Further arguments for EmitLight function: Vec3, which is the map position of this "scripted bulb". (Just place temporarily a random dummy object there in the map, and read the object coordinates on the object info panel.) 20, the last argument value is for the radius.

    Naturally this setup works even without a table:

    Code:
    -- FILE: Levels\My_level.lua
    
    function FlameAmbience()
        local flameOn = 0  
        for i = 1, 10 do
           	if GetMoveableByName("flame_emitter_" .. i):GetActive() == true then
               	flameOn = flameOn + 1
    	end    	
        end
        EmitLight(Vec3(12288, -1280, 10086),
        	Color(200 * flameOn / 10, 100 * flameOn / 10, 20 * flameOn / 10), 20) 	
    end
    
    LevelFuncs.OnLoad = function() end
    LevelFuncs.OnSave = function() end
    LevelFuncs.OnStart = function() end
    LevelFuncs.OnLoop = function()
    	FlameAmbience()
    end
    LevelFuncs.OnEnd = function() end

"Pairs" and "ipairs" functions

You will use pairs or ipairs initial LUA functions for "for" loops:
  • when the loop will do something with a table, and
  • when the loop parameters are not a starting value, an ending value (and optionally that how much the value changes), but the indices and the values of this table.
For example:

Code:
-- FILE: Levels\My_level.lua

dogrooms = {"Dog Room1", "Dog Room2", "Dog House", "Dog Arena", "Dog Lair"}

function RoomPrint(roomtable)
	for i, v in ipairs(roomtable) do
		local pos = i * 100 - 100
		ShowString(DisplayString(v .. " has ID" .. i, 300, 300 + pos,
			Color(255,0,0)), 10)    
	end
end

LevelFuncs.OnLoad = function() end
LevelFuncs.OnSave = function() end
LevelFuncs.OnStart = function()
	RoomPrint(dogrooms)
end
LevelFuncs.OnLoop = function() end
LevelFuncs.OnEnd = function() end
When the level starts, then this function will list the rooms where Lara will meet dogs in the level, mentioning the room name (v) and the index the room has in "dogrooms" table (i):

Dog Room1 has ID1 in position (300, 300)
Dog Room2 has ID2 in position (300, 400)
Dog House has ID3 in position (300, 500)
Dog Arena has ID4 in position (300, 600)
Dog Lair has ID5 in position (300, 700)

So the argument of "ipairs" function is the table name. This time "i" variable value is the regular index of a table value. The "for" loop will check them one by one, in an ascending order: 1, 2, 3, 4 etc. The value of "v" variable is a value of the table, which belongs to the current index. (Again, i and v only default names, you can change them, if you want.)
Naturally you can do that even with a "casual" "for" - but only if you know exactly that how many values (5) you want to check in the table:

Code:
function RoomPrint(roomtable)
	for i = 1, 5 do
		local pos = i * 100 - 100
		ShowString(DisplayString(roomtable[i] .. " has ID" .. i, 300, 300 + pos,
			Color(255,0,0)), 10)    
	end
end
The example above with "ipairs" function also works with "pairs" function - but basically you should use "pairs" when the table indices are irregular (in which case the setup is useless with "ipairs"):

Code:
-- FILE: Levels\My_level.lua

dogrooms = {["r1"] = "Dog Room1", ["r2"] = "Dog Room2", 
	["h"] = "Dog House", ["a"] = "Dog Arena", ["l"] = "Dog Lair"}

function RoomPrint(roomtable)
	for k, v in pairs(roomtable) do
		if k == "r1" then
			pos = 300
		elseif k == "r2" then
			pos = 400
		elseif k == "h" then
			pos = 500
		elseif k == "a" then
			pos = 600
		else
			pos = 700
		end		
		ShowString(DisplayString(v .. " has ID" .. k, 300, pos,
			Color(255,0,0)), 10)    
	end
end

LevelFuncs.OnLoad = function() end
LevelFuncs.OnSave = function() end
LevelFuncs.OnStart = function()
	RoomPrint(dogrooms)
end
LevelFuncs.OnLoop = function() end
LevelFuncs.OnEnd = function() end
Dog Room1 has IDr1 in position (300, 300)
Dog Room2 has IDr2 in position (300, 400)
Dog House has IDh in position (300, 500)
Dog Arena has IDa in position (300, 600)
Dog Lair has IDl in position (300, 700)

And here is a complex example with "ipairs" - the game will place a flaming effect on the head top of the dogs which are just in "Dog Room1" or "Dog Room2", removing the effect while they are just out of these rooms.

Click image for larger version

Name:	98.jpg
Views:	24
Size:	87.4 KB
ID:	7616

The problem with this setup is that this effect is harmless to Lara. (Or even for the dogs themselves - but let's say the dogs are resistant to their own flames.) That is why we enhance the features of this effect with some additional scripting:
  • When Lara is near the flaming heads, then the hotness of the flame will hurt her a bit.
  • When Lara touches a "head flame", then she will catch fire.
So in these rooms the dogs don't need to bite her to kill her...

Code:
-- FILE: Levels\My_level.lua


function FlamingDogHeads()
	local doggroup = {}
	for dog_i = 1, 7 do
		doggroup[dog_i] = GetMoveablesBySlot(ObjID.DOG)[dog_i]
	end
	
	local meshgroup = {}
	for mesh_i = 1, 15 do
		meshgroup[mesh_i] = Lara:GetJointPosition(mesh_i - 1)
	end	

	for index1, dog in ipairs(doggroup) do
		local room = dog:GetRoom()
	
		if dog:GetStatus() == MoveableStatus.ACTIVE then
			if room:GetName() == "Dog Room 1" or room:GetName() == "Dog Room 2" then

				local headmesh = dog:GetJointPosition(3)
				local flamepos = Vec3(headmesh.x, headmesh.y - 100, headmesh.z)
				EmitFire(flamepos, 1.5)	

				local larapos = Lara:GetPosition()			
				if larapos:Distance(flamepos) <  768 then
					Lara:SetHP(Lara:GetHP() - 1)
				end
			
				for index2, laramesh in ipairs(meshgroup) do	
					if laramesh:Distance(flamepos) <  100 then
						Lara:SetEffect(EffectID.FIRE)
					end
				end			
			end
		end	
	end 
end

LevelFuncs.OnLoad = function() end
LevelFuncs.OnSave = function() end
LevelFuncs.OnStart = function() end
LevelFuncs.OnLoop = function()
	FlamingDogHeads()
end
LevelFuncs.OnEnd = function() end
The function for this feature (FlamingDogHeads) will be called in OnLoop callback. Which means that this time we will check in each frame of the playtime that:
  • If the dogs are in the required room. If they are then they'll have flames on their head. Otherwise they won't.
  • If Lara is near a dog having a flaming head. If she is then she will be hurt a bit. Otherwise she won't.
  • If Lara is touching a head flame. If she is, then she will catch fire. Otherwise she won't.
As a first step, FlamingDogHeads function will create two custom tables:
  • Preset TEN functions often create unnamed tables. These tables are not custom, made by you, so they don't need a {} line to define that table. I mean, in their cases calling the function is what creates that table.
    When the argument of this function is a group, then the group members will be the values of the table.
    See eg. "GetMoveablesBySlot" preset function now. Its argument is ObjID.DOG, which means the function argument is the group of all the dogs placed in this level map, so the values of this table are these dogs.
    Tables like that always have regular index values: 1, 2, 3, 4 etc.
    But you create a "doggroup" custom table now, which also has these dogs as table values. This time you placed seven dogs in the map, which is why the "for", which will fill this custom empty table with the dogs, will make i (I mean: dog_i now) variable run from 1 to 7.
    That is why

    doggroup[dog_i] = GetMoveablesBySlot(ObjID.DOG)[dog_i]

    means that:
    The dog in "doggroup" custom table at ID1 will be the dog, also with ID1, in the table created by GetMoveablesBySlot TEN preset function.
    The dog in "doggroup" custom table at ID2 will be the dog, also with ID2, in the table created by GetMoveablesBySlot TEN preset function.
    Etc.
  • The second "for" loop also has a custom table - but this time we use a preset function which won't create a table:
    "Lara:GetJointPosition" preset function will identify a mesh position of Lara. For this purpose, the argument of the function is the ID number of that mesh. Mesh indices always run from 0, so when we say that LARA object has fifteen meshes (you can identify them in WadTool), then they IDs run from 0 to 14.
    But you create a "meshgroup" custom table now, which also has these mesh positions as table values. Naturally usually we should use regular indices for a table. That is why the "for", which will fill this custom empty table with the mesh positions, will make i (I mean: mesh_i now) variable run from 1 to 15.
    That is why

    meshgroup[mesh_i] = Lara:GetJointPosition(mesh_i - 1)

    means that:
    The mesh position in "meshgroup" custom table at ID1 will be the current position of Lara's mesh, identified as Mesh0, by Lara:GetJointPosition TEN preset function.
    The mesh position in "meshgroup" custom table at ID2 will be the current position of Lara's mesh, identified as Mesh1, by Lara:GetJointPosition TEN preset function.
    Etc.
After this, the main part of the FlamingDogHeads function starts - the ipairs function about these dogs:

for index1, dog in ipairs(doggroup) do

The "for" loop will take the dogs of "doggroup" table, one by one, once in each moment of OnLoop. Unlike a "casual" "for", this is a place where you don't need to calculate "X to Y" intervals for the indices, because the loop will end now automatically (for this moment of OnLoop) when it has just counted all the dogs:
First it takes the dog at ID1. So the value of "dog" variable where "index1" value is 1.
Then it takes the dog at ID2. So the value of "dog" variable where "index1" value is 2.
Etc.

Now this is what this "for" will do with the current dog, in the current moment of OnLoop:
  • First it defines the room where this dog is currently there:

    local room = dog:GetRoom()

  • Then it checks a "condition in condition" for the current dog, where the essence of FlamingDogHeads happens for this dog. (I.e. when the condition is true, then it won't execute something, but another condition will be checked.)
This "condition in condition" checks if the current dog is already living and if it is in the required rooms (Dog Room1 or Dog Room2).
I mean, the game seemingly nicely handles the issues of inactive objects. I mean, eg. even if the dog is placed inside Dog Room1 and Dog Room2, then the game won't place a flame effect on the invisible head of the dog, before it is getting spawned.
On the other hand, I experienced a bug about this: when a dog like this is already living, with a flaming head, but I go back to the title without killing it. In this case, if I restart the level now from the title, without quitting the game, then one or more "phantom flames" will be floating on invisible dog heads, before they are getting spawned in these rooms.
That is why I used "dog:GetStatus() == MoveableStatus.ACTIVE" to check if the current dog is already living when I also check the required rooms. (Which naturally will give the same true or false answer now, as if I used GetActive().)

This complex condition technically could be typed even only in one row, like:

if room:GetName() == "Dog Room 1" or room:GetName() == "Dog Room 2" and dog:GetStatus() == MoveableStatus.ACTIVE then

But I had a good reason two separate them into two conditions: one will "wrap" the other one inside in it.
I mean, you cannot type those conditions in the same row, because the result could be wrong, the phantom flames will be still there. - That is why I first check if the dog is living, I only check its current room only after that: a "condition in condition". Now there will surely not be phantom flames.

So if the current dog is in one of the required rooms (and living), then it can get that flame effect on its head:
  • First the game defines the current position of the headmesh of this dog, stored in headmesh variable (check in WadTool, that this mesh has ID3):

    local headmesh = dog:GetJointPosition(3)

  • Positions are also tables created by preset TEN functions. I mean, see a previous example above: we used "Vec3(12288, -1280, 10086)" function to set a position for EmitLight "light bulb". Now Vec3 function uses three arguments, as x, y and z coordinate values. Each coordinate is a value of the table created by this function.
    That is why dog:GetJointPosition(3) will also create x, y and z coordinates (as the current position of the current dog Mesh3). So now, just like GetMoveablesBySlot, the function has one argument to create a table with more values. - Which is why "headmesh" is not a variable, but a table - even if it looks like a variable, without {} brackets.
    The differences of GetJointPosition from GetMoveablesBySlot:

    • This time the argument is not a group (dogs), but one simple element (a mesh).
    • The table size is not the unknown size of the group (seven dogs, less, more? - only the game will define it), but three well-known properties (x, y, z coordinates) of that only one element (mesh).

    TEN preset function-made tables, with well-identified properties don't have regular (1, 2, 3, 4 etc.) index values. Instead they have some well-identified, unique, irregular index values. In fact, tables about 3D positions always have textual indices, named as "x", "y" and "z".

    However, instead of the one line of "local headmesh = dog:GetJointPosition(3)" line, you could use four lines. I mean, you could define the headmesh table in the usual way, also putting the values of the preset function made-table, one by one, into the custom table, in further three lines (remember that ["index"] and .index mean the same):

    Code:
    local headmesh = {}
    headmesh["x"] = dog:GetJointPosition(3).x
    headmesh["y"] = dog:GetJointPosition(3).y
    headmesh["z"] = dog:GetJointPosition(3).z
    Whatever method you choose, the headmesh coordinates in headmesh variable (I mean, table...) are not exactly proper for us. I mean, a mesh position usually defined in the pivot of the mesh, in the middle of the mesh. That is why the flame should be placed not exactly in the mesh position, but a bit higher.
    The vertical coordinate is the "y" one. I lift that coordinate up with 100 units, so the flame will be placed on the top of the head. - And, because vertical coordinates are inverted (I mean, negative values are above 0 level, and positive ones are below that), that is why I won't add this value to the vertical mesh coordinate, instead I will subtract it from it.
    This fixed value will be stored in flamepos variable (I mean, flamepos table). Which means that Vec3 function will use the three arguments (the original "x" and "z" headmesh values, and the new "y" value) to define a new position, to store it there:

    local flamepos = Vec3(headmesh.x, headmesh.y - 100, headmesh.z)

  • Then EmitFire preset TEN function will place the flame effect on this corrected position, where 1.5 means the effect size:

    EmitFire(flamepos, 1.5)
The corrected flame position will be calculated for each dog of that "for-ipairs" loop.
The EmitFire function will ignite the flame only for one moment - but that is not a problem, because the next moment of OnLoop will keep it burning further, ignite it for a further one moment, then again in the next moment etc. - Except: if the dog is not in any of the required rooms any more, then the flame won't be ignited again (till the dog comes back into these rooms).

Now that "condition in condition" starts checking Lara (i.e. if the current dog is in one of the required rooms and living):
  • First Lara's current position will be stored in larapos variable (again: it is really a table):

    local larapos = Lara:GetPosition()

    Her position seemingly set mostly at where her vertical axis hits the ground where she is just walking/running/standing.
  • We want Lara to be hurt a bit when she is near a head flame. We don't need to be so exact now, that is why I compare not a mesh of her to that flamepos position, but her "general position", stored in larapos.
    I chose that the three quarters of a sector is enough to define "near", which is why the game will check if Lara's current position is closer to the current flamepos position of the current dog, than 768 units. (Because 1 sector = 1024 units, and 1024 x 3/4 = 768.)

    if larapos: Distance(flamepos) < 768 then

  • So Lara will loose a bit of health (1 HP at each tick frame) when she is near. (Her maximum HP is 1000, when her health bar is full.)
    It is scripted with forcing a new HP value on her, where the function argument value is her current HP value now (set in the form of another preset function now), reduced by 1 (i.e. by 0.1 percent):

    Lara:SetHP(Lara:GetHP() - 1)
Then that "for-ipairs" loop will check it even with another dog for Lara, in that moment. And then with the further dogs of the moment.
Then in the further moments of OnLoop, it will check all the dogs one by one again and again on her, for this.

Now that "condition in condition" continues checking Lara (i.e. if the current dog is in one of the required rooms and living), but now for something else - to set her on fire or not:
  • Another "for-ipairs" loop also runs here. It will take Lara's mesh positions of "meshgroup" table, one by one, once in each moment of OnLoop:
    First it takes Mesh0 position at ID1. So the value of "laramesh" variable where "index2" value is 1.
    First it takes Mesh1 position at ID2. So the value of "laramesh" variable where "index2" value is 2.
    Etc.

    for index2, laramesh in ipairs(meshgroup) do

  • Now this is what this "for" will do in the current moment of OnLoop:
    It compares again Lara's position to the current flamepos position of the current dog. But this time it is not Lara's general position, but the position of the current mesh.
    I mean, this time we need to be more exact: we want Lara to catch fire when any of her meshes touches the headflame. (So this time it is not about her mesh touching the dog headmesh - because mesh collisions haven't been implemented into TEN yet, when I am writing this tutorial. - Anyway, in this situtation it wouldn't be proper to us.
    Why? Because we want her to catch fire only when her mesh touches the headmesh at the top - not eg. at the mouth, where there is no flame. That would be the best if we check when her mesh touches the flame body. But that is impossible now with a simple TEN function - and doing it by a tiresome manual position calculation is something which I didn't want even to try in this setup.)
    So the method I wanted to use is: checking when Lara's currently checked mesh position and flamepos position (i.e. the top of the head, i.e. the bottom of the flame) are the same.
    However, I couldn't do it. I mean, the mesh has some thickness, so these two positions can never be the same, because the mesh position is inside the mesh. I.e. Lara's mesh position will never touch flamepos position. - So, instead, I checked that the distance between these positions is less than 100 units.
    I.e. I calculated 100 units as a general thickness. So, technically it is probably the same as if Lara's mesh touching the headtop there:

    if laramesh: Distance(flamepos) < 100 then

  • So Lara will catch fire, when she is too close:

    Lara:SetEffect(EffectID.FIRE)
Then her "for-ipairs" loop will compare the position of her next mesh to the current flamepos position of the current dog. And then the positions of her further meshes, as well.
Then the "for-ipairs" loop of the dogs will jump to the next dog, so her "for-ipairs" loop will compare her meshes even to that dog. And the dogs' loop will jump to the further dogs, to check also there the distances.
Then in the further moments of OnLoop, it will check all the dogs one by one again and again on her, for this.
(So checking her meshes continue even if she is already burning, so the game tries to set her on fire again, unnecessarily. Oh, well. I mean, she will die soon, anyway. Or, if she extinguishes the flame in the meanwhile, then that is naturally a good reason to continue the checking, to ignite her again.)

"While" loops

For example our "tiresome" example to print "Hello, Lara!" six times, looks this way with a "while" loop:

Code:
LevelFuncs.OnStart = function()
	local i = 0
	while i ~= 600 do
		local printLara = DisplayString("Hello, Lara!", 250, 250 + i, Color(255,0,0))
		ShowString(printLara, 10)
		i = i + 100
	end
end
As you can see, the i variable is still local - though this time "local" is declared. Interesting, but this time you declare its local variable out of the loop block. (Which is why this time this i is available even out of that block. Naturally in this example i is available in the whole OnStart callback.)
The first i value is 0, so the first "Hello, Lara" will be printed in (250, 250 + 0 = 250) position.
Then the loop will turn i into 100, with the i = i + 100 addition.
Then the actual step of the loop ends, the loop starts the next step, first printing the second "Hello, Lara" in (250, 250 + 100 = 350) position.
Etc.
The last (sixth) "Hello, Lara!" will be printed in (250, 250 + 500 = 750) position. I mean, the i variable value won't turn in this step into 600 with i = i + 100, because we said in the condition that i cannot be 600. - So the loop has been stopped now. (Naturally I could write "i < 600" now instead of "i ~= 600": "i must be less than 600".)

Or let's see our example with the flaming dog heads. This time we use a "while" loop to fill the "doggroup" table with seven dogs (naturally I could write "i ~= 8" now instead of "i < 8"):

Code:
local doggroup = {}
local dog_i = 1
while dog_i < 8 do
	doggroup[dog_i] = GetMoveablesBySlot(ObjID.DOG)[dog_i]
	dog_i = dog_i + 1
end
As you can see, "for" and "while" loops technically will do the same. The difference is:
  • Usually you should choose "for", when you know exactly that when the loop should be ended.
  • Usually you should choose "while", when you don't know exactly that when the loop should be ended (or, at least, it is complicated or tiresome to calculate it). So, instead of telling the ending value, you will tell the value which cannot be reached ever.
For example, in the example of "Hello, Lara!", you will tell exactly that the "for" loop should be stopped at i = 500.
On the other hand, for the "while" loop, you "only" tell that i cannot reach 600 (i < 600). Because the step is 100 now, and you don't want i to turn from 500 to 600. But, for this reason, any number is a proper limit now for a < relation, between 501 and 600: i < 501, i < 502, i < 503, ... i < 598, i < 599, i < 600. So, with an approximate value, you will stop the loop at the proper point.

"Repeat/until" loops

"Repeat/until" loops are also about unclear ending values, just like "while" loops. The difference is:
  • A "while" loop tells that the loop should be stopped everyway BEFORE reaching an approximate value. (I.e. this is a maximum upper limit.)
  • A "repeat/until" loop tells that the loop should be stopped everyway AFTER (or WHEN) reaching an approximate value. (I.e. this is a minimum upper limit.)
For example:

Code:
LevelFuncs.OnStart = function()
	local i = 0
	repeat
		local printLara = DisplayString("Hello, Lara!", 250, 250 + i, Color(255,0,0))
		ShowString(printLara, 10)
		i = i + 100
	until i > 514
end
The loop starting line contains only the "repeat" tag, there is no condition here, there is no "do" here. Plus, this time the loop block is closed by an "until" tag, not an "end" tag.
This time the condition should be typed in the line of "until". Again: 500 should be the highest i, 600 shouldn't be used. So you can stop the loop by an approximate minimum limit, if the limit value is i > 500, i > 501, i > 502, i > 503, ... i > 598, i > 599.
This time I intentionally chose a random limit now: i > 514.
(But "until i == 600" is also a proper value now, to stop the loop, not printing a next line in 600 position. I.e. this is the case when the loop works in WHEN mode, not AFTER mode.)

"Break" statements

"Break" statements are actions in additional conditions of the loops (in any loop type). The purpose of a "break" is to stop the loop before it reaches the limit defined in i variable.
See the six lines above about "Hello, Lara!" text, when you use a "while" loop.
This time we expand that example. I mean, you say now that you don't want to print after the position of 500, even if you originally wanted to print six lines on the screen. So you don't want/cannot calculate that 500 limit is enough for this or not.
That is why you'll use a "break" not to print the lines over that position - also printing a warning on the screen, if you'd exceed this limit with further printings:

Code:
LevelFuncs.OnStart = function()
	local i = 0
	while i ~= 600 do
		local pos = 250 + i
		if pos > 500 then
			ShowString(DisplayString("Too long list!", 250, 600, Color(255,0,0)), 10)
			break
		end
		local printLara = DisplayString("Hello, Lara!", 250, 250 + i, Color(255,0,0))
		ShowString(printLara, 10)
		i = i + 100
	end
end
So this code would print seemingly only three lines of "Hello, Lara" on the screen, printing below that that the list is too long. (Please keep in mind that you are not allowed to print lines in the "if" block of the "break", below "break". So if you'd type that ShowString function after the break, not before that, then that function would be ignored.)

"Goto" statements

The "goto" statement is usually used in loops, but it is not mandatory.
The "goto" makes the game jump to a specific place of the script, when just reading the line of "goto". This place is called a "label", and the line of "goto" will name this label. Repeat the name in the script, between two :: signs: this is the place of the label.

Code:
LevelFuncs.OnStart = function()
	for i = 0, 500, 100 do
		if i == 200 or i == 300 then
			goto emptyline
		end
		local printLara = DisplayString("Hello, Lara!", 250, 250 + i, Color(255,0,0))
		ShowString(printLara, 10)
		::emptyline::
	end
end
See eg. this version of printing "Hello, Lara!". This time we avoid printing "Hello, Lara!" at 450 (250+200) and 550 (250+300) coordinates, jumping to "emptyline" label before printing, so there will be two empty lines, at those coordinates.

However, in this case probably it is an easier version of the setup:

Code:
LevelFuncs.OnStart = function()
	for i = 0, 500, 100 do
		if i < 200 or i > 300 then
			local printLara = DisplayString("Hello, Lara!", 250, 250 + i, Color(255,0,0))
			ShowString(printLara, 10)
		end
	end	
end
----------

Notes:
  • There can be even descending loops, like:

    for i = 5, 0, -1 do

  • Notes to the flaming dog heads example:

    • I intentionally use custom names this time for "for" i, k, v variables.
    • As you can see, there are different variable names for the same table index, in the case of "for" loops of "doggroup". This index is called "dog_i" when we created the table, but it is called "index1" for ipairs.
      Never mind, you can name loop variables any time as you wish. I mean, eg. if the value of this variable is 1, then "doggroup[variable_name]" will always refer to the table value at the first index of the table, whatever variable_name is.
      However, feel free to call them even the same, for the same table, even in different blocks. But naturally now each loop will care about only its own variable.
    • Please notice that in the case of ipairs/pairs, the current value of the table is simply marked by the current name of "v" variable in the loop. See eg.: "dog:GetRoom()", when the current value is "dog", the current name of "v".
      However, if it is a "casual" "for", then you mark this current value by the table name and the table index, which index is the current name of "i" variable in the loop, eg.: "doggroup[dog_i]".
    • The custom variables and tables of the setup are all local. I mean, we don't need LevelVars this time, to restore the values for savegames, because these variables and tables will refresh their values from preset TEN functions, again and again, each moment of the playtime.
    • You may ask: "Why do we create these custom tables in FlamingDogHeads function? I mean, the tables could be global and LevelVars, typed out of the function, in OnStart callback. So the tables will be created and filled when the level starts, and the function will use these custom table values - always restored from savegames, if necessary - in OnLoop". - Like this:

      Code:
      -- FILE: Levels\My_level.lua
      
      function FlamingDogHeads()
      	for index1, dog in ipairs(LevelVars.doggroup) do
                   (...)
      	end 
      end
      
      LevelFuncs.OnLoad = function() end
      LevelFuncs.OnSave = function() end
      LevelFuncs.OnStart = function()
      	LevelVars.doggroup = {}
      	for dog_i = 1, 7 do
      		LevelVars.doggroup[dog_i] = GetMoveablesBySlot(ObjID.DOG)[dog_i]
      	end
      	
              LevelVars.meshgroup = {}
      	for mesh_i = 1, 15 do
      		LevelVars.meshgroup[mesh_i] = Lara:GetJointPosition(mesh_i - 1)
      	end	
      end
      LevelFuncs.OnLoop = function()
      	FlamingDogHeads()
      end
      LevelFuncs.OnEnd = function() end
      No, you are wrong. I mean, remember: LevelVars variables (or table values) cannot be used for identified things. Like objects or meshes.
      Yes, I know that usually we don't need to create and fill these empty tables, again and again, at each time of the playtime, when their contents are constant. (Because the amount of dogs placed on the level won't change and Lara's mesh amount won't change either.) But, again, you can't say it should be enough to create them once, when the level starts, because they are identified things.
      So I placed these script lines inside FlamingDogHeads function - which is why they will be called continuously (defining the same tables again and again), just like the other lines of the function.
      (I.e. the table which was made full in the first moment is emptied for the second moment - because the empty table is created again -, and, in that moment, the loop fills it again with values. And again and again, in the further moments.)
    • As I said, headmesh table doesn't need to be created with the usual way, with a {} line.
      And you could use the same method even for doggroup table, to set values in it - using the fact, that GetMoveablesBySlot has already did that table, with regular indices. So you can simply copy/paste those values with their regular indices, into a custom table.
      So, instead of all of this:

      Code:
      local doggroup = {}
      for dog_i = 1, 7 do
          doggroup[dog_i] = GetMoveablesBySlot(ObjID.DOG)[dog_i]
      end
      use simply this (i.e. you don't need to care about either table indices now in [] brackets or the loop or the table creation with a {} line):

      Code:
      local doggroup = GetMoveablesBySlot(ObjID.DOG)
    • In the case of the custom table creations, the two sides of the equations tell the same: dogs and dogs, Lara meshes and Lara meshes. Why are they duplicated?
      Because, as you know, custom tools always look better than preset tools, because custom tools are specified for your current custom function, so you can use and understand them more easily, in the further part of the custom function contents. - That is why I chose to use custom tables now, instead of using directly those preset function values, copying the preset function values into custom tables.
      However, the original setup would look this way when custom tables are removed:

      Code:
      function FlamingDogHeads()
      	for index1, dog in ipairs(GetMoveablesBySlot(ObjID.DOG)) do
      		local room = dog:GetRoom()
      	
      		if dog:GetStatus() == MoveableStatus.ACTIVE then
      			if room:GetName() == "Dog Room 1" or room:GetName() == "Dog Room 2" then
      
      				local headmesh = dog:GetJointPosition(3)
      				local flamepos = Vec3(headmesh.x, headmesh.y - 100, headmesh.z)
      				EmitFire(flamepos, 1.5)	
      
      				local larapos = Lara:GetPosition()			
      				if larapos:Distance(flamepos) <  768 then
      					Lara:SetHP(Lara:GetHP() - 1)
      				end
      			
      				for index2 = 0, 14 do
      					laramesh = Lara:GetJointPosition(index2)
      					if laramesh:Distance(flamepos) <  100 then
      						Lara:SetEffect(EffectID.FIRE)
      					end
      				end			
      			end
      		end	
      	end 
      end
    • Please, note that if room:GetName() == "Dog Room 1" or "Dog Room 2" would be a bad scripting.
      I.e. even if the second value is also for "room:GetName()", you need to name this for both the values.
    • Remember, that ":" sign in the preset custom name means there is the function subject written before this sign. So, if we have a chain of subjects, then we don't need to split them - only if you want to make them look nice.
      So, eg. instead of using this nice scripting:

      Code:
      for index2 = 0, 14 do
      	laramesh = Lara:GetJointPosition(index2)
      	if laramesh:Distance(flamepos) <  100 then
      		Lara:SetEffect(EffectID.FIRE)
      	end
      end
      you can use even this, i.e. using two ":" signs in the same line:

      Code:
      for index2 = 0, 14 do
      	if Lara:GetJointPosition(index2):Distance(flamepos) <  100 then
      		Lara:SetEffect(EffectID.FIRE)
      	end
      end

  • Please don't misunderstand when I say that "while" and "repeat/until" are for unclear endings.
    I mean, naturally it is meaningless to type this when you have only seven dogs placed in the map:

    Code:
    local doggroup = {}
    for dog_i = 1, 100 do
    	doggroup[dog_i] = GetMoveablesBySlot(ObjID.DOG)[dog_i]
    end
    But that doesn't mean it ruins the setup. I mean, the game will be able to fill the custom table only with the seven existing dogs, the other indices of the table will remain empty.
    So you can use even "for" for an unclear ending.

    However, it is not always a lucky choice. I mean, eg. in this case:

    Code:
    LevelFuncs.OnStart = function()
    	for i = 0, 700, 100 do
    		local printLara = DisplayString("Hello, Lara!", 250, 250 + i, Color(255,0,0))
    		ShowString(printLara, 10)
    	end
    end
    the game will print on the screen naturally not six, but eight lines, so the ending value of "for" remains clear everyway.
  • Here is an example when you use something else instead of "break" statement, to break the running loop:
    There are four bears placed in the level. They are all sauntering around Room 0. If any of them is just enters Room 0, then the game will kill it - and randomly zero, one or more further bears. Then it can be repeated with the remaining living bears (waiting for the further bears to enter Room 0), till they all die.

    Code:
    -- FILE: Levels\My_level.lua
    
    function KillingBears()
    	local bears = {}
    	if killbears == nil or killbears == false then
    		for index = 1, 4 do
    			bears[index] = GetMoveablesBySlot(ObjID.BEAR)[index]
    			local room = bears[index]:GetRoom()
    			if room:GetName() == "Room 0" and bears[index]:GetHP() > 0 then
    				stop = index
    				killbears = true
    			end
    		end
    	end
    	if killbears == true then
    		for counter = 1, stop do
    			bears[counter]:SetHP(0)			
    			if counter == stop then
    				killbears = false
    			end
    		end
    	end
    end
    
    LevelFuncs.OnLoad = function() end
    LevelFuncs.OnSave = function() end
    LevelFuncs.OnStart = function() end
    LevelFuncs.OnLoop = function()
    	KillingBears()
    end
    LevelFuncs.OnEnd = function() end
    First you create one empty table: bears.
    But then the essence of the function happens after that, in two condition blocks: the upper block checks if killbears variable is nil or false, and the lower block checks if this variable is true.
    When the function starts then killbears variable is still undefined (nil), i.e. it cannot be either true or false - which is why the content of the upper condition will be executed now. Which means that a four values long loop will be executed now - for the four bears:

    bears[index] = GetMoveablesBySlot(ObjID.BEAR)[index]

    So the loop will fill "bears" custom table with the bears found by GetMoveablesBySlot function.
    When the first bear has been put into this table, then the next line will define the current room of this current bear:

    local room = bears[index]:GetRoom()

    Then the next line will start an "if" block to check if the current bear is in Room 0, and if this bear hasn't died yet:

    if room:GetName() == "Room 0" and bears[index]:GetHP() > 0 then

    I will tell below that why it is important to check if that bear is living. (Naturally they are still all living now - except eg. if Lara has shot at least one of them yet.) Now you need to understand only that, if the condition tells that the current bear is in this room, that means we have the first bear has just entered Room 0, so it is time for the game to kill it - and randomly even further zero, one, two or three bears.
    If this "if" is false (so if the current bear is in a different room), then the contents of this "if" block will be skipped. - As a next step, these can be possible now:

    • The for i = 1, 4 do loop is not at its last value (4): now the loop jumps to the next i value, to put the next bear into "bears" table.
    • The for i = 1, 4 do loop is at its last value (4): it means the game has just done what it needs to do in the current moment in the if killbears == nil or killbears == false block. The result: all the bears are in the table, but none of them is in Room 0.
      The game will go further in the function, checking now the lower condition block of if killbears == true then. This is also false now (I mean, "killbears" is not "true" now), so this round of the function ends. The next moment of OnLoop is coming, the function jumps back to its start: if killbears == nil or killbears == false then is still true, so its contents will be executed again, starting with creating the empty bears table again (i.e with emptying the existing bears table now). Etc.

    But if that "if" about the room is true (so if the current bear is in Room 0), then:

    • There are four cases possible for the living bear just entering Room 0:

      • This bear is at table ID1. So this bear is the only bear in the "bears" table of the current moment.
      • This bear is at table ID2. So there is already one more other bear in the "bears" table of the current moment.
      • This bear is at table ID3. So there are already two more other bears in the "bears" table of the current moment.
      • This bear is at table ID4. So there are already all the four bears of the level in the "bears" table of the current moment.

    • We need the current index value out of this "for" loop. But, as you know, "index" variable is local, so we now copy its value into "stop" global variable:

      stop = index

    • Then we define, at last, "killbears" global variable, which was undefined (nil) so far. The variable value will be "true" - which means that the upper condition block isn't true any more for if killbears == nil or killbears == false then, which is why the "for i = 1, 4 do" loop will be interrupted here, further bears won't be stuffed into "bears" table any more.

      killbears = true

    So the contents in the lower condition block of if killbears == true then will be executed now (because "killbears" is "true" now) :
    This "if" starts another loop now:

    for counter = 1, stop do

    It runs from 1 to "stop", i.e. to index value where the previous loop was interrupted:

    • From 1 to 1. In this case the bears table has only one bear.
    • From 1 to 2. In this case the bears table has two bears.
    • From 1 to 3. In this case the bears table has three bears.
    • From 1 to 4. In this case the bears table has four bears.

    This loop checks all the (one, two, three or four) bears of the bears table:

    • The current bear will be killed:

      bears[counter]:SetHP(0)

    • Then the loop continues, killing the further bears of the table. - So, when all the bears of the table are checked, then all the bears of the table must be dead.
      But the loop needs to run only once now. So, when "counter" variable value reaches, even the first time, the last value of the loop (i.e. "stop") - but after killing the bear at that index -, then you need to prevent this loop from starting again:

      if counter == stop then

    • The method of this prevention is turning "killbears" variable into "false". This makes if killbears == true then false, which is why this loop in this condition cannot run again.

      killbears = false

    So this round of the function ends, after killing some bears. So, it is time to run the contents again of if killbears == nil or killbears == false then condition, in the next moment of OnLoop (because killbears is "false" now):

    • First for i = 1, 4 do loop starts filling again the bears table with the bears of the map. (Either if they are still living, or the setup - or else - killed them previously.)
    • Then the game defines again the current room of the current bear.
    • And this is the point when checking HP in if room:GetName() == "Room 0" and bears[index]:GetHP() > 0 then condition becomes important.
      I mean, previously we killed a bear in Room 0, so its dead body is still there in Room 0. But we want to ignore that bear now. I mean, we run this function now for a living bear that just enters that room, so we also need this HP condition now.
    • So, filling the table will be interrupted again (if the current, living bear is in Room 0), with one, two, three or four bears in the table. (Again, it can have even bears already died.) - I won't tell again what happens if none of the (living) bears is not in Room 0 now.

    So, after that interruption, we run the contents of if killbears == true condition again. All the living bears in the table will be killed now - but naturally the dead bears of the table cannot be killed once more.
    And then we go back again to execute if killbears == nil or killbears == false then condition contents, in the next moment of OnLoop.
    Etc.

    A few thoughts to this:

    • Probably you'll need a condition or something in OnLoop, to stop running unnecessarily KillingBears function further, when all the bears are already dead.
    • "If killbears == true" is not the opposite of "if killbears == nil or killbears == false", so, instead of this:

      Code:
      if killbears == nil or killbears == false then
           (...)
      end
      if killbears == true then
           (...)
      end
      you can't type this:

      Code:
      if killbears == nil or killbears == false then
           (...)
      else
           (...)
      end
      And, anyway it also seems uselesss:

      Code:
      if killbears == nil or killbears == false then
           (...)
      elseif killbears == true then
           (...)
      end
    • "Killbears" variable is not LevelVars, so it will lose its value at saving/loading. But it is not a problem, because:

      • If it loses its value when that is "false", then the value will be "nil" - but "false" and "nil" are both useable for the same condition, in this setup.
      • Technically it cannot lose its value when that is "true". I mean, when it is "true", then the bears will be killed immediately, and the value turns into "false". So it is only a moment, you don't have time enough to start saving a savegame, while it is true.

    • The setup is not as random as it looks at first sight.
      I mean, bears seemingly will be set in their table always in the same order. So eg. "Bear_A" always go to ID2, "Bear_B" always go to ID3, "Bear_C" always go to ID4 and "Bear_D" always go to ID1. Which means that eg. if Bear_A is the first bear entering Room 0, then always the two same bears will be killed in that moment: Bear_A and Bear_D.
    • If you do not copy index variable into stop variable, so instead you use the last index (#bears) in "if killbears == true", then that doesn't seem a good solution.
      I mean, it seems that the last index always uses the absolute last index of the table, if that is set by a loop. (It is 4 now, defined by "for i = 1, 4 do".) But we need a relative last index now (1, 2, 3 or 4, depending on when the loop is interrupted).

Last edited by AkyV; 13-11-24 at 09:45.
AkyV is online now  
Old 13-09-24, 23:33   #4
AkyV
Moderator
 
Joined: Dec 2011
Posts: 5,074
Default

3. Multiple assignments

You have already read about multiple assignments in this tutorial. - Just remember table.unpack function:
A, B and C variables will take values from "NatlasTeam = {"Natla", "Pierre", "Larson"}" table, in the same order. So the first variable (A) will take the first table value (Natla), the second variable (B) will take the second table value (Pierre) etc.
Or the example when this function turned the table values into function arguments.

Let's see some further examples, to understand that how many similar things can be done with multiple assignments:

local X = 48
local A, B = 163, 6 * X, 82


Now A is 163, B is 288. The value of 82 will be ignored.

local A = 26
local B = 42
A, B = B, A


Now A is 42, B is 26.

local NatlasTeam1 = {"Natla", "Pierre", "Larson"}
local NatlasTeam2 = {"Kold", "Kid", "Cowboy"}
NatlasTeam1[1], NatlasTeam2[1] = NatlasTeam2[1], NatlasTeam1[1]


Now NatlasTeam1[1] is Kold, NatlasTeam2[1] is Natla.

local flame = TEN.Objects.GetMoveableByName("flame_emitter2_117" )
local A, B = "Hello, Lara!", flame:GetOCB()


Now A is "Hello, Lara!", B is the current OCB value of the Moveable object with flame_emitter2_117 name.

There can be some more complex situations. Keep reading the tutorial for some more info.

----------

Note:

local _, A = "Hello, Lara!", flame:GetOCB()

means that I don't care about "Hello, Lara" text, I only want to record the flame OCB, in variable A.
I.e. _ means that is a "dummy variable". However, this doesn't mean _ is not a valid variable name, so, after all, "Hello, Lara!" will be recorded in it.

Last edited by AkyV; 03-11-24 at 12:42.
AkyV is online now  
Old 02-10-24, 23:15   #5
AkyV
Moderator
 
Joined: Dec 2011
Posts: 5,074
Default

4. Advanced mathematical operations

The initial LUA functions to make advanced mathematical operations:
  • It tells the minimum value in a group of numbers:

    math.min (number1, number2, number3, ...)

    It tells the maximum value in a group of numbers:

    math.max (number1, number2, number3, ...)

    Examples:

    math.min(500, 49, 7743) → 49

    or

    local X = 7743
    local numbers = {500, 49, X}
    math.max(table.unpack(numbers))
    → 7743

  • Rounding up to the nearest integer value:

    math.ceil(floating_number)

    Rounding down to the nearest integer value:

    math.floor(floating_number)

    Examples:
    5.2 rounded up → 6
    -5.2 rounded up → -5
    5.2 rounded down → 5
    -5.2 rounded down → -6

    A workaround to round down 632 to the nearest hundred (600):

    math.floor(632 / 100) * 100

    632 / 100 = 6.32 → rounded down by the function = 6 → 6 * 100 = 600
  • This function will split a floating number into an integer value and a fractional one (so this function has two results):

    math.modf(floating_number)

    Example:

    local A, B = math.modf(549.78) → A = 549, B = 0.78

    So, as you can see, if a function has multiple results, then you need multiple assignments to copy these results into variables.

    Using the function for integer numbers is possible, but meaningless:

    local A, B = math.modf(549) → A = 549, B = 0.0

  • Calling the function without arguments, it creates a random floating number between 0 and 1, with many digits (like eg. 0.21661376953125):

    math.random()

    Calling the function with one argument (a positive integer number), it creates a random integer number between 1 and the argument (eg. typing 5, the values can be: 1, 2, 3, 4, 5) - technically you can type even 1 for the argument, but naturally that is meaningless:

    math.random(positive_integer_number_above_1)

    Calling the function with two arguments (two integer numbers: negative, zero or positive), it creates a random integer number between the arguments (eg. typing 2 and 5, the values can be: 2, 3, 4, 5):

    math.random(integer_number_lower_limit, integer_number_upper_limit)

    A workaround to create random floating numbers, with one digit after the decimal point, in the (1.0, 5.0) interval:

    math.random(10, 50) / 10

    So remember: creating a random number always includes the borders of the interval. So eg. creating a random number in the (1, 5) interval can create 1, 2, 3, 4 or 5 random numbers.
  • This function is the opposite of "tostring", because it turns a number, typed as some text, into a number:

    math.tointeger(text)

    For example, the result of math.tointeger("500") is 500.
    But keep in mind that it works only for integer number arguments. If the argument is letters or a floating number, then the result will be nil, like math.tointeger("Hello, Lara!") or math.tointeger("500.82").

    But there is another function for this conversion. It is not a "math" function, though. - However, what matters is it is able to convert even floating numbers in text form into a "real" floating number:

    tonumber("500") → 500
    tonumber("500.82") → 500.82

    Moreover, it is able to convert even binary (2) or hexadecimal (16) numbers, into decimal ones (notice that you still need to do it from a textual form):

    tonumber("11111001100", 2) → 1996
    tonumber("7CC", 16) → 1996

    Or an alternative solution for the hexadecimal conversion (using 0x or 0X prefix instead of marking it as "16"):

    tonumber("0x7CC") → 1996

    0b or 0B prefix for binary numbers are not accepted in LUA.
  • With this function, you can check the type of the argument:

    math.type(X)

    If X is some text, then the result is nil, if an integer number, then integer, or if a floating number, then float.
  • This function will tell the distance of something, compared to 0:

    math.abs(X)

    So, eg. if X is either 17.5 or -17.5, the result will be 17.5.
    For example: you want to check in your custom function that how far Lara is from a base position. You don't care if Lara is in this direction (17.5 sectors) or in that direction (-17.5 sectors) from that position, what you care is only the length of this distance (17.5 sectors) from that position.
  • Trigonometry:

    • The value of pi is used often in trigonometry. This constant (not a function, not having () brackets for arguments) will give the exact value of pi:

      math.pi

    • The trigonometric functions will use radians, not degrees, to calculate angles. But naturally degrees are more comfortable to you. - Luckily it is easy to convert one into the other:

      angle_in_degrees = angle_in_radians * 180 / math.pi

      angle_in_radians = angle_in_degrees * math.pi / 180


      Or if you don't want to do it manually, then:
      Convert radians to degrees:

      math.deg(angle_in_radians)

      Convert degrees to radians:

      math.rad(angle_in_degrees)

    • Draw (or, at least) imagine a right angled triangle on some distances:

      • If you know at least the length of one triangle side, and at least the size of one non-right angle, then you can calculate the length of the other sides, with the trigonometric functions.
      • If you know at least the length of two triangle sides, then you can calculate the radians (or degrees) of the two non-right angles, with the trigonometric functions.

      Click image for larger version

Name:	99.jpg
Views:	36
Size:	81.3 KB
ID:	7666

      The main trigonometric functions mean the ratio between the lengths of different triangle sides:

      • Sine of the checked angle: opposite / hypotenuse
      • Cosine of the checked angle: adjacent / hypotenuse
      • Tangent of the checked angle: opposite / adjacent

      You need the inverse of the main trigonometric functions, to get the size of the angle from that main function:

      • Arcsine is the inverse of sine.
      • Arccosine is the inverse of cosine.
      • Arctangent is the inverse of tangent.

      Their LUA functions are:

      • Sine:

        math.sin(checked_angle_in_radians)

      • Cosine:

        math.cos(checked_angle_in_radians)

      • Tangent:

        math.tan(checked_angle_in_radians)

      • Arcsine:

        math.asin(sine_of_checked_angle)

      • Arccosine:

        math.acos(cosine_of_checked_angle)

      • Arctangent:

        math.atan(tangent_of_checked_angle)

      Examples:

      • The checked angle is 30 degrees. The opposite side is 7.25 sectors. You want to know the length of the hypotenuse side:
        Sine is opposite / hypotenuse, so hypotenuse = 7.25 / sine of 30 degrees = 7.25 / 0.5 = 14.5 sectors:

        local hypotenuseSide = 7.25 / math.sin(math.rad(30))

      • The opposite side is 5.75 sectors. The adjacent side is 9.5 sectors. You want to know the size of the checked angle:
        Tangent is opposite / adjacent, so the checked angle is the arctangent of 5.75 / 9.5 = (approximately) 31.18 degrees.

        local angleDegrees = math.deg(math.atan(5.75 / 9.5))
----------

Notes:
  • If the value is between -1 and 1, then you don't need to type 0 before the decimal point, the result will be nicely handled, printed:

    0.15 * -0.42 = .15 * -.42 = -0.063

  • Some additional thoughts about these calculations:

    • In the case of typing only "local A = math.modf(549.78)" (i.e. only one variable), only the first result, i.e. 549 now, would be realized, the fractional part would be lost. (Anyway, the result is the same, if you don't copy the result into a variable, simply typing "math.modf(549.78)".)

      This is why this function is nicely useable to turn an unnecessarily floating number into an integer.
      I mean, eg. as you already know (see my basic scripting tutorial), the result of a division is always a floating number, even if that could be an integer: eg. 4 / 2 = 2.0.
      But with this method you can cut the unnecessary fractional part off the number:

      math.modf(4 / 2) → 2

      On the other hand, if you want to turn an integer into a floating number, then - it is not hard to find it out - just divide it by 1, eg.:

      2 / 1 = 2.0

    • Naturally it is very unlikely that you use binary or hexadecimal values as texts. So, if they are not texts, then they are not ready for a "tonumber" conversion, first you need to convert them into texts (for hexadecimal numbers, only the prefixed way is accepted):

      local A = 11111001100
      local B = 0x7CC

      tonumber(tostring(A), 2)
      tonumber(tostring(B))

      It looks really funny when you want to print the decimal result on the screen, so first you turn the result number back to text, using another "tostring", like:

      tostring(tonumber(tostring(A), 2))

    • Naturally you can use math.pi not only in trigonometry. - See eg. the area of a circle:

      diameter ^ 2 * math.pi / 4

    • The aggregate of the angles in a right angled triangle is naturally 180 degrees. So if you used trigonometric functions to calculate one of the non-right angles, then you can easily get the other one:

      non-right_angle2 = 180 - 90 - non-right_angle1 = 90 - non-right_angle1

    • If you know the length of two sides in a right angled triangle, then naturally you can use Pythagoras' theorem to calculate the third side:

      hypotenuse ^ 2 = adjacent ^ 2 + opposite ^ 2

  • Not only math.pi is the only constant you can use for mathematical purposes:

    • Technically use this constant for the value of "infinite":

      math.huge

      In fact it refers to a real number, which is not infinite, but it is ssssooooo huuuuge that we can bravely use it as infinite.
      The value of the constant will be printed as inf. (But never type inf yourself.)

      Some thoughts:

      • Dividing by zero (eg. 100 / 0) also has the same value.
      • "Minus infinite" is naturally -math.huge.
      • Naturally many operations are possible with math.huge, but meaningless (math.huge * 5, math.huge / 500 etc.) Their result is always math.huge.
      • Some operations with math.huge (eg. math.huge * 0) will have -nan(ind) result. This is an abbreviation for "not a number, indeterminate". (Why minus...?) However, math.type(math.huge * 0) has the result of "float", i.e. that is not a "not a number". (Probably it "only" means it is different than other numbers.)

    • This constant means the smallest (integer) value which LUA is able to handle:

      math.mininteger

      It is naturally bigger than -math.huge (i.e. closer to zero), but it is still really-really-really small (and naturally negative). (I.e. its absolute value is really-really-really big.)
      Probably you should use this constant in a relation, if you use a really tiny negative number, but you naturally don't want to cross this limit: "is my number less than this"? (Practically it is implausible that you can get into a situation like this.)
    • This constant means the biggest (integer) value which LUA is able to handle:

      math.maxinteger

      It is naturally smaller than math.huge (i.e. closer to zero), but it is still really-really-really big (and naturally positive).
      Probably you should use this constant in a relation, if you use a really huge positive number, but you naturally don't want to cross this limit: "is my number bigger than this"? (Practically it is implausible that you can get into a situation like this.)

  • Functions with issues:

    • According to my experiences, the result for math.modf won't always be exact.
      Eg. in the case of "local A, B = math.modf(549.78)" you may experience that A will be really 549, but B won't be 0.78. Instead, B will be something else, like 0.77999999999997.
      So, if that value is important for you to be exact, and you want to be sure that the function will give the proper value, then use it a bit differently, like:

      local A, B = math.modf(549.78)
      local B = math.ceil(B * 100) / 100


      0.77999999999997 * 100 = 77.999999999997 → math.ceil(77.999999999997) = 78 → 78 / 100 = 0.78

    • Theoretically there is a short way to compare the size (i.e. their absolute value) of two integer numbers to each other:

      math.ult(integer_number_1, integer_number_2)

      The problem is I had some crucial bad test results with this function, so I did not even mention it in the list above.
      I mean, the result should be "true" if the absolute value of the first number is the smaller number. Otherwise the result should be "false".
      I've got a nice true result with the example of math.ult(5, -6): "five is less than six". But I've also got the same true result eg. for math.ult(5, -4): "five is less than four".
      So, if you want a relation like this, then I suggest a custom function instead:

      Code:
      LevelFuncs.OnStart = function()
      
      	function Relations(A, B)
      		if math.abs(A) < math.abs(B) then
      			return true
      		else
      			return false
      		end
      	end
      
      	ShowString(DisplayString(tostring(Relations(5, -4)), 300, 300, Color(255,0,0)), 10)
      
      end
      So you should see nicely a "false" printed on the screen, when the level starts.
    • Theoretically math.random function will always give nicely a random number, as it should - but if it happens more than one time in the same cycle (eg. in the same playtime), then it will be always the same (range of) random number(s). Thankfully there is math.randomseed (seeding_value) function, which should be run before math.random, so math.random will nicely always give different (range of) random number(s) for each running.
      However, it seems you don't need math.randomseed in TEN, so the results of math.random will be differently random, for each running, even without math.randomseed. Perhaps is math.randomseed embedded in the main TEN code? I don't know, and it doesn't matter. What matters is feel free to ignore using it.
    • Some further trigonometric functions (cosecant, secant, cotangent, arcsecant, arccosecant, arccotangent) don't have initial LUA versions. However, you don't really need them, it is enough if you use the ones I listed above.
    • Mathematical functions seeming deprecated (useless) in the LUA version that TEN uses:

      • This function should do the same as the basic mathematical operation of the exponentiation I mentioned in my basic scripting tutorial:

        math.pow(A, B)

      • Hyperbolic functions (math.cosh, math.sinh, math.tanh) are also missing.
        Oh well, they are surely irrelevant for the most of the level builders.
      • There was a version for math.atan (math.atan2), in the case of the arctangent having two arguments.
        Math.atan should work even with two arguments nowadays, instead of using math.atan2 - but I don't think it would be a helpful function for a casual level builder.
      • I don't even understand what these two functions did before, so I just mention them: math.ldexp, math.frexp.
      • I am not sure if this function is deprecated here, but I surely couldn't run it: it is math.log10 for the base-10 logarithm.

  • Available functions, but not listed above:

    • I did not list these functions above, because they can be done even with some basic mathematical operation you have already learnt in my basic scripting tutorial (see back there, for more info):

      • Modulo (remainder):

        math.fmod(A, B)

      • Calculating roots - but the function will work only for the square root:

        math.sqrt(X)

        So eg. if X = 81, then the result will be 9.

    • I did not list these functions above, because casual level builders will probably won't use them - that is why I won't tell examples to them now:

      • It calculates the exponential value of a number, i.e. the exponential constant (called usually "e" in mathematics, approximately it is 2.718281828459045) is raised to that number:

        math.exp(raising_e_to_this_number)

      • This function calculates the natural logarithmic value of a number:

        math.log(number)

  • "Relations" above is a function having one result - but it can be either true or false.
    If you want two results for your custom function, like above with "local A, B = math.modf(549.78)", then type two values in the return line, separated by a comma, like:

    Code:
    function Operations(X, Y)
    	local A = X + Y
    	local B = X * Y
    	return A, B
    end
    
    local num1 = 6
    local num2 = 10
    local add, multipl = Operations(num1, num2)
    ShowString(DisplayString(tostring(add) .. " " .. multipl, 300, 400, Color(255,0,0)), 10)
    Now 16 and 60 will be nicely printed on the screen. (A and B could be local, because you'll never call them directly out of the function.)
    But if you type simply "Operations(num1, num2)", then what you'll get will be not a multiple result. (Like when you typed simply "math.modf(549.78)" above.) I mean, now only the first result, 16 will be printed:

    ShowString(DisplayString(tostring(Operations(num1, num2)), 300, 400, Color(255,0,0)), 10)

  • Feel free to create custom functions for the non-existing LUA mathematical initial functions, like these examples (with regular table indices now):

    • The amount of elements is even (true) or odd (false):

      Code:
      function EvenAmount(table_name)
      	if math.floor(#table_name / 2) * 2 == #table_name then
      		return true
      	else
      		return false
      	end
      end
      We use the fact now that the amount of table values is the last index.
      So, for example, if the last index (#table_name) of the table is 4, then 4 / 2 = 2.0. That is 2 rounded down. 2 multiplied by 2 is exactly the last index (4). So the result is: true.
      But, for example, if the last index (#table_name) of the table is 5, then 5 / 2 = 2.5. That is 2 rounded down. 2 multiplied by 4 is not the last index (5). So the result is: false.
    • Average:

      Code:
      function Average(table_name)
      	local sum = 0
      	for i = 1, #table_name do
      		sum = sum + table_name[i]	
      	end
      	return sum / #table_name
      end
      So the 0 value of sum will be increased by each value of the table. (Because the last index says: "stop increasing only at the end of the table".) Again: the last index also means the value amount in the table. So the final value of sum divided by this amount is the average.
    • Median - the middle element of values, if they are in descending/ascending order. If the amount of the values is even, then the median is the average of the two middle elements:

      Code:
      function Median(table_name)
      	table.sort(table_name)
      	local middle = math.ceil(#table_name / 2)
      	if EvenAmount(table_name) == true then
      		return (table_name[middle] + table_name[middle + 1]) / 2
      	else
      		return table_name[middle]
              end
      end
      Let's see for example this table: {5, 5, 1, 4, 3}. Table.sort arranges it into ascending order: {1, 3, 4, 5, 5}.
      The last index of the table (5) divided by 2 is 2.5. That rounded up is 3: this is the value of "middle" variable.
      EvenAmount custom function says this table has odd (5) values, so the return value now is the value of the arranged table at ID3: it is 4.

      Or, let's see for example this table: {5, 5, 1, 2, 4, 3}. Table.sort arranges it into ascending order: {1, 2, 3, 4, 5, 5}.
      The last index of the table (6) divided by 2 is 3.0. That rounded up is 3: this is the value of "middle" variable.
      EvenAmount custom function says this table has even (6) values, so the return value now is the average of the value at ID3 (3) and the value at ID4 (4), which is 3.5.

      Naturally you need to run only the median function in the proper place (eg. in OnStart). I mean, the even/odd function will be called automatically as the contents of the median function.

Last edited by AkyV; 07-11-24 at 17:50.
AkyV is online now  
Old 03-10-24, 19:34   #6
AkyV
Moderator
 
Joined: Dec 2011
Posts: 5,074
Default

5. Bitwise operations

In the case of bitwise operations you do your things with binary numbers.

If we'd like to explain the binary numbers, based on decimal numbers, then we need to say that the digits of the binary numbers are composed of the powers of 2. Each digit like that is called "bit", their ID is that power value:

the first digit = Bit0 = 2 to power of 0 = 1
the second digit = Bit1 = 2 to power of 1 = 2
the third digit = Bit2 = 2 to power of 2 = 4
the fourth digit = Bit3 = 2 to power of 3 = 8
the fifth digit = Bit4 = 2 to power of 4 = 16
Etc.

The value of that bit is 0, if that bit is unset, or 1, if it is set.
The bits should be typed in a descending order - for example:

6-----5-----4---3---2---1---0 → powers of 2 (bit ID)
64---32---16---8---4---2---1 → decimal bit value
1-----0----1----1---0---0---1 → bit set/unset

If you want to know the decimal value of a binary number, then sum the decimal values of all the bits, which bits are set:
So the decimal value of this binary number (1011001) is 64 + 16 + 8 + 1 = 89. (Really naturally you don't need this manual conversion. I mean, I mentioned the converter function in the previous chapter.)

Anyway, you can easily calculate the amount of the needful bits of a binary number, searching for the largest power which fits that value. So eg. now 64 (6) can be stuffed in 89, but the next power (7, with 128 value) is not. So there will be seven digits now: 0, 1, 2, 3, 4, 5, 6.

Bitwise "AND" operation

The OCB (Object Code Bit) values of an object could be even very simple: 0, 1, 2, 3, 4 etc. So eg. 0 is for the basic behavior, and it will execute some specific thing if OCB1 is typed at its object panel, or it will execute some other specific thing if OCB2 is typed etc.
But that doesn't work in many cases: if the object should have even more than one specific behavior sometimes, at the same time. I mean, what if eg. you want the object have specific behaviors marked by both OCB1 and 2 values? Then how can you mark it? With their sum: 1 + 2 = 3? But what if there is already another behavior set at OCB3?

The solution is available with the bitwise "AND" operation. The operation is marked by AND word or symbol. (Which is not the ^ symbol of the LUA exponentiation.)
Technically it works like multiplying two decimal numbers by each other. But this time you multiply the bit of a binary number by the same bit of another binary number. For example, multiplying the 0/1 value of Bit3 at Number A by the 0/1 value of Bit3 at Number B.
Naturally there could be only three operations: 0 ∧ 0, 0 ∧ 1, 1 ∧ 1. As I said they work like decimal multiplications (0 x 0 = 0, 0 x 1 = 0, 1 x 1 = 1), so the results could be: 0 ∧ 0 = 0, 0 ∧ 1 = 0, 1 ∧ 1 = 1. - I.e. the result will be "set"/"unset" if both the bits are set/unset. Otherwise it is "unset".
Let's try it with these two numbers now: 89 (1011001) above, and 923 (1110011011) - naturally you need to fill the shorter number with unnecesary 0 values, so the length of the two numbers will be the same:

9-----8-----7------6-----5-----4---3---2---1---0 → powers of 2 (bit ID)
512--256--128---64---32---16---8---4---2---1 → decimal bit value
0-----0-----0------1-----0----1----1---0---0---1 → 89
1-----1-----1------0-----0----1----1---0---1---1 → 923
0-----0-----0------0-----0----1----1---0---0---1 → 25

So the result is 89 AND 923 = 25, not 89 x 923 = 82147.

When I say that this operation is a solution for multiplied OCB values, that means that OCB values will be the decimal bit values: 0, 1, 2, 4, 8, 16 etc., instead of 0, 1, 2, 3, 4 etc.
And why is it good? Let's say we want the multiplied OCB values of both OCB 2 and 8. Their sum is 10 - and there is no OCB defined at the value of 10. Besides, there is no other sum, which can be 10: eg. 1+8, 1+4, 2+4, 1+4+8 etc. are surely not 10. - So if the sum is 10, then you can be sure, that OCB2 and 8 are set (and only they are set) at this object.
This means that only Bit1 and Bit3 are set at that object, for the OCB values of the object. - So when you adjust a multiplied OCB for an object, then you type this sum (like 10 now) on its OCB panel.

If you want to check if a multiplied OCB has a specific OCB value, then you check if a bit is set or unset at that value:
Does OCB1 is used? No, Bit0 cannot be set in 10.
Does OCB2 is used? Yes, Bit1 is set in 10.
Does OCB4 is used? No, Bit2 cannot be set in 10.
Does OCB8 is used? Yes, Bit3 is set in 10.
Etc.

The bitwise "AND" operation will do this check this way, eg. for OCB=8 now:

3---2---1---0 → powers of 2 (bit ID)
8---4---2---1 → decimal bit value
1---0---1---0 → OCB sum = 10
1---0---0---0 → checked OCB = 8
1---0---0---0 → result = 8

So 10 AND 8 = 8.
Which means:
  • If OCB sum has X OCB, then the result is X OCB (or: is not 0).
  • If OCB sum doesn't has X OCB, then the result is not X OCB (or: is 0) - like:

    4---3---2---1---0 → powers of 2 (bit ID)
    16--8---4---2---1 → decimal bit value
    1---0---1---1---1 → OCB sum = 23
    0---1---0---0---0 → checked OCB = 8
    0---0---0---0---0 → result = 0
In LUA scripts, we use & symbol instead of the general AND/∧ signs. - For example we check now, if the current OCB sum of the object (which object is stored in "object" variable now) has OCB 8:

if object:GetOCB() & 8 == 8 then

I.e. it checks the result of "OCB sum AND 8".

A full condition - if the OCB sum has OCB 8, then remove this OCB from there, not hurting the other OCBs of the sum:

Code:
if object:GetOCB() & 8 ~= 0 then
     object:SetOCB(object:GetOCB() - 8)
end
Or the opposite - if the OCB sum doesn't have OCB 8, then set this OCB there, not hurting the other OCBs of the sum:

Code:
if object:GetOCB() & 8 == 0 then
     object:SetOCB(object:GetOCB() + 8)
end
(Please, keep in mind that seemingly some OCB values can be set only via the object panel, in TE. - I mean, it seems that some OCB values of some object slots cannot be controlled via scripting.)

Custom OCB values

Technically you can easily set a custom OCB value. After all, all you eg. need is a condition which checks if a bit is set in the OCB. If that is true, then the condition will execute that custom thing - something like this:

Code:
if object:GetOCB() & X == X then
     (...)
end
But it may cause bugs in working of the object/that custom thing. Besides, what would you do, if later the developers put an "official" OCB on the value you already use for a custom task?

That is why I suggest leaving OCB only for official using.
Instead, let's create the term of "Custom Code Bit" (CCB), which is useable not only for Moveable objects, but for anything which can be exactly identified in the map: Statics, rooms, sinks etc.

Let's see for example the example again with the flaming dog heads. This time we don't care about nice flame positions - so this time (to have a simple example) we place the flames on the mesh pivots. (And this time these will be harmless flames.)
Let's see first the case when you want only maximum one flame per dog, so you don't need multiplied CCBs:
We use three CCB numbers, to place flames on three different meshes of dogs: back (mesh1, CCB1), head (mesh3, CCB2), tail end (mesh25, CCB3). (But it is not a must. You could choose even 25, 68, 192 CCB values for it. Whatever.)

Code:
-- FILE: Levels\My_level.lua

function SetDogCCB()
	local dogs = {}
	for index = 1, #LevelVars.dogtableCCB1 do
		dogs[index] = TEN.Objects.GetMoveableByName(LevelVars.dogtableCCB1[index])
		if dogs[index]:GetStatus() == MoveableStatus.ACTIVE then				
			local backmesh = dogs[index]:GetJointPosition(1)
			EmitFire(backmesh, 1.5)
		end
	end
	for index = 1, #LevelVars.dogtableCCB2 do
		dogs[index] = TEN.Objects.GetMoveableByName(LevelVars.dogtableCCB2[index])
		if dogs[index]:GetStatus() == MoveableStatus.ACTIVE then				
			local headmesh = dogs[index]:GetJointPosition(3)
			EmitFire(headmesh, 1.5)
		end
	end
	for index = 1, #LevelVars.dogtableCCB3 do
		dogs[index] = TEN.Objects.GetMoveableByName(LevelVars.dogtableCCB3[index])
		if dogs[index]:GetStatus() == MoveableStatus.ACTIVE then				
			local tailendmesh = dogs[index]:GetJointPosition(25)
			EmitFire(tailendmesh, 1.5)
		end
	end	
end

LevelFuncs.dogswap = function()
	table.move(LevelVars.dogtableCCB2, 1, #LevelVars.dogtableCCB2, #LevelVars.dogtableCCB1 + 1, LevelVars.dogtableCCB1)
	table.move(LevelVars.dogtableCCB3, 1, #LevelVars.dogtableCCB3, #LevelVars.dogtableCCB1 + 1, LevelVars.dogtableCCB1)
	for i = 1, #LevelVars.dogtableCCB2 do
		LevelVars.dogtableCCB2[i] = nil
	end
	for i = 1, #LevelVars.dogtableCCB3 do
		LevelVars.dogtableCCB3[i] = nil
	end
end

LevelFuncs.OnLoad = function() end
LevelFuncs.OnSave = function() end
LevelFuncs.OnStart = function()
	LevelVars.dogtableCCB1 = {"dog_14", "dog_15", "dog_16"}
	LevelVars.dogtableCCB2 = {"dog_17", "dog_20"}
	LevelVars.dogtableCCB3 = {"dog_18", "dog_19"}

end
LevelFuncs.OnLoop = function()
	SetDogCCB()
end
LevelFuncs.OnEnd = function() end
When the level starts, then the game creates three saveable (LevelVars) custom tables for dogs. Each table has different dog names. DogtableCCB1 table is naturally for dogs having flame on their backs, dogtableCCB2 table is naturally for dogs having flame on their heads, dogtableCCB3 table is naturally for dogs having flame on their tailends.
SetDogCCB function will run in OnLoop, putting the one frame long flame in each moment on the current position of these meshes. There are three loops in the function, each for one CCB value. (I used independent loops, ignoring elseifs or else. Probably it is clearer this way now, to explain.)
I also made that "dogs" custom table to make the things easier. (So you don't need to refer that long "GetMoveableByName(LevelVars.dogtable...[index])" way to the dogs in this function, again and again.) Each loop uses dogs table, but it is not a problem: because when one loop needs it, then the other ones don't. The current loop will overwrite the values of the previous loop, in this table. (DogtableCCB1 is longer than the other two tables. But it is not a problem, either. Because loops uses the last indices of their own tables. So when loop2 and then loop3 goes maximum to ID2, then they will ignore the value of DogtableCCB1 left at ID3.)
The way the loops place the flames on the dogs, should be familiar to you, from "Loops" chapter.

There also is a LevelFuncs.dogswap function. This is called as a volume (local) event set, through TE. When Lara (or else) activates this volume trigger, then the contents of the LevelVars tables changes: the full contents of dogtableCCB2 and dogtableCCB3 tables will be copy/pasted into dogtableCCB1 table, then both dogtableCCB2 and dogtableCCB3 will be fully emptied.
The result: all these dogs still living will swap their flame, if that is just not on their back, so all the head flames and tailend flames will be removed to the backs. (I.e. eg. this is a way to change a CCB value.)

This setup naturally looks differently when you want to use these CCB values for multiplied cases (so eg. when a dog can have flames not only on one body part at the same time):

Code:
-- FILE: Levels\My_level.lua

function SetDogCCB()
	local dogs = {}
	local doggroup = GetMoveablesBySlot(ObjID.DOG)
	for i, dog in ipairs(doggroup) do
		CCB = 0
		for index = 1, #LevelVars.dogtableCCB1 do
			dogs[index] = TEN.Objects.GetMoveableByName(LevelVars.dogtableCCB1[index])
			if dog == dogs[index] then
				CCB = 1							
			end
		end
		for index = 1, #LevelVars.dogtableCCB2 do
			dogs[index] = TEN.Objects.GetMoveableByName(LevelVars.dogtableCCB2[index])
			if dog == dogs[index] then
				CCB = CCB + 2
			end
		end
		for index = 1, #LevelVars.dogtableCCB4 do
			dogs[index] = TEN.Objects.GetMoveableByName(LevelVars.dogtableCCB4[index])
			if dog == dogs[index] then
				CCB = CCB + 4
			end
		end			
		if CCB & 1 == 1 then
			if dog:GetStatus() == MoveableStatus.ACTIVE then				
				local backmesh = dog:GetJointPosition(1)
				EmitFire(backmesh, 1.5)
			end
		end
		if CCB & 2 == 2 then
			if dog:GetStatus() == MoveableStatus.ACTIVE then				
				local headmesh = dog:GetJointPosition(3)
				EmitFire(headmesh, 1.5)
			end
		end			
		if CCB & 4 == 4 then
			if dog:GetStatus() == MoveableStatus.ACTIVE then				
				local tailendmesh = dog:GetJointPosition(25)
				EmitFire(tailendmesh, 1.5)
			end
		end
	end
end

LevelFuncs.OnLoad = function() end
LevelFuncs.OnSave = function() end
LevelFuncs.OnStart = function()
	LevelVars.dogtableCCB1 = {"dog_14", "dog_15", "dog_16", "dog_19"}
	LevelVars.dogtableCCB2 = {"dog_15", "dog_16", "dog_17", "dog_20"}
	LevelVars.dogtableCCB4 = {"dog_16", "dog_17", "dog_18", "dog_19"}

end
LevelFuncs.OnLoop = function()
	SetDogCCB()
end
LevelFuncs.OnEnd = function() end
This time the table names (CCB1, 2, 4) at the level start refer to that now we will use multiplied OCBs, because they are the powers of 2. An object can be set even in more than one table this time. (This time we won't change the table contents, to change the CCB of an object. I mean, in the previous example I introduced that how you can do that.)
This time there is a main loop, in which we will check all the dogs placed in the level. The sub-loops in the main loop will check the contents of each LevelVars.dogtableCCB... table:
  • First the main loop tells that CCB = 0. Which means that each moment of the playtime, the currently checked dog will be handled initially as not having any Custom Code Bit set. (It is a global variable. I mean, it would be a local enough for this loop. But the further definitions of CCB should be global everyway, so I also set it as global.)
  • Then the first sub loop runs, checking all the dogs in LevelVars.dogtableCCB1 table. If any dog in the table is the current dog of the main loop, then that dog gets first CCB value (1), instead of 0.
  • Then the second sub loop runs, checking all the dogs in LevelVars.dogtableCCB2 table. If any dog in the table is the current dog of the main loop, then that dog gets CCB2 value. (So its CCB sum could be 2 or 3, depending on it has got CCB1 in the previous sub loop, or not.)
  • Then the third sub loop runs, checking all the dogs in LevelVars.dogtableCCB4 table. If any dog in the table is the current dog of the main loop, then that dog gets CCB4 value. (So its CCB sum could be 4, 5, 6 or 7, depending on it has got CCB1 and/or CCB2 in the previous sub loops, or not.)
After that the main loop checks three "if"s, one after the other:
  • If the CCB sum has Bit0 (1), then the dog will have a flame on its back.
  • And/or if the CCB sum has Bit1 (2), then the dog will (also) have a flame on its head.
  • And/or if the CCB sum has Bit2 (4), then the dog will (also) have a flame on its tail end.
In this setup we have seven dogs, with (unintentionally) also seven different situations: back, head, tail, back+head, back+tail, head+tail, back+head+tail.

Bitwise "OR" operation and custom bit flags

There could also be an "OR" bitwise operation between the same bits of two binary numbers. The operation is marked by OR word or symbol. (Which is not the "v" letter.)
If any of the bits is set, then the result is "set", but if none of them is set, then the result is "unset": 0 ∨ 0 = 0, 0 ∨ 1 = 1, 1 ∨ 1 = 1.
For example:

9-----8-----7------6-----5-----4---3---2---1---0 → powers of 2 (bit ID)
512--256--128---64---32---16---8---4---2---1 → decimal bit value
0-----0-----0------1-----0----1----1---0---0---1 → 89
1-----1-----1------0-----0----1----1---0---1---1 → 923
1-----1-----1------1-----0----1----1---0---1---1 → 987

So the result is 89 OR 923 = 987.

This method should be also familiar, even in the classic Tomb Raider level building: when you used a group activation to activate Moveable objects. (Or flipmaps.)
I mean, in these cases you had to be careful with the five bit options (Bit1, 2, 3, 4, 5: "bit flags") set or unset on the object panel and/or the trigger panel - because the object has been activated when all of these options have been set:
  • If a bit was ticked on the object panel, then that bit is consired as "set".
  • If a bit was ticked on the trigger panel, and that trigger has been activated, then that bit is consired as "set".
For example, in this setup you needed to activate both the triggers (for opening the door), to open this door:
  • "Trigger" with Bit1 and Bit5 ticked, Bit2, Bit3, Bit4 unticked.
  • "Pad" trigger with Bit3 and Bit4 ticked, Bit1, Bit2, Bit5 unticked.
  • The door with Bit2 and Bit3 ticked, Bit1, Bit4, Bit5 unticked.
The bits of the door are initially set. Let's suppose you activate first the "Pad" trigger for the door (Bit0 is unused this time, so it should be considered as a constant 0):

5-----4----3---2---1---0 → powers of 2 (bit ID)
32---16---8---4---2---1 → decimal bit value
0----0-----1---1---0---0 → Door: 12
0----1-----1---0---0---0 → Pad: 24
0----1-----1---1---0---0 → Result: 28

Bit1 and Bit5 is still unset, so the door still won't open. (It is meaningless to set Bit3 at Pad now, because that is already set at the door object. However, it won't ruin the procedure.)

Then we also activate the "Trigger":

5-----4----3---2---1---0 → powers of 2 (bit ID)
32---16---8---4---2---1 → decimal bit value
0----1-----1---1---0---0 → Door+Pad: 28
1----0-----0---0---1---0 → Trigger: 34
1----1-----1---1---1---0 → Result: 62

Now Bit1, Bit2, Bit3, Bit4 and Bit5 are all set, the door will open.

Or aggregated:

5-----4----3---2---1---0 → powers of 2 (bit ID)
32---16---8---4---2---1 → decimal bit value
0----0-----1---1---0---0 → Door: 12
0----1-----1---0---0---0 → Pad: 24
1----0-----0---0---1---0 → Trigger: 34
1----1-----1---1---1---0 → Result: 62

So 12 OR 24 OR 34 = 62. (So 62 = all the five bits are set.)

TEN supports this classic feature. However, it is not a TEN feature - so if you want more control on this (eg. to check that which bits are already set), then you need to write a fully custom TEN script for this.
However, its advantage is that in a TEN script like that you can use even more than five bits (including Bit0), so you can create even a more complex setup: eg. pull ten (or even more) switches to open a door.
This example will use seven big wall mechanical levers (the most classic Tomb Raider switch) to open a door. As I said, originally it is not a TEN feature, so you can't use bit flags on the object panel or on the trigger panel for this TEN script - but, as you can see just below, you don't need them, anyway:

Code:
-- FILE: Levels\My_level.lua

function OpenDoor()
	bitflags = 0
	if GetMoveableByName("switch_type4_17"):GetAnim() == 1 then
		bitflags = 1
	end
	if GetMoveableByName("switch_type4_18"):GetAnim() == 1 then
		bitflags = bitflags + 2 
	end
	if GetMoveableByName("switch_type4_19"):GetAnim() == 1 then
		bitflags = bitflags + 4 
	end
	if GetMoveableByName("switch_type4_20"):GetAnim() == 1 then
		bitflags = bitflags + 8 
	end
	if GetMoveableByName("switch_type4_21"):GetAnim() == 1 then
		bitflags = bitflags + 16
	end
	if GetMoveableByName("switch_type4_22"):GetAnim() == 1 then
		bitflags = bitflags + 32
	end
	if GetMoveableByName("switch_type4_23"):GetAnim() == 1 then
		bitflags = bitflags + 64 
	end	
	if bitflags & 1 == 1 and bitflags & 2 == 2 and bitflags & 4 == 4
		and bitflags & 8 == 8 and bitflags & 16 == 16 and bitflags & 32 == 32 and bitflags & 64 == 64 then
		GetMoveableByName("door_type4_16"):Enable()
	else
		GetMoveableByName("door_type4_16"):Disable()
	end
end

LevelFuncs.OnLoad = function() end
LevelFuncs.OnSave = function() end
LevelFuncs.OnStart = function() end
LevelFuncs.OnLoop = function()
	OpenDoor()
end
LevelFuncs.OnEnd = function() end
These are switches, so we need the usual triggers (Switch for the switch and a Trigger for the door) on the sectors of the switches, so the switches will work as they should. However, with this script, we don't need any classic triggers at all. - You can solve this contradiction by placing that Switch trigger and another, fake Trigger (eg. for an Animating, which will never move, if you activate it) on the sector of the switch.
So this is what happens now: first the function (running in OnLoop) will declare that bitflags variable is 0 initially, in each moment of the playtime. (Again: it has nothing to do with the bit flags of trigger and object panels.)
Animation1 of these switches is the one frame long animation when the switch is in "on" position. So seven "if"s check the seven switches if they are in this position. If one of them is just performing Animation1, then that switch will add a bit to bitflags variable. - If all of them is performing that animation, then the eighth "if" opens the door (checking if all those bits are set) - otherwise it remains open. (Or if that has been already opened, then it will be closed, when Lara moves a switch back to "off" position, not adding that bit again to bitflags variable, in the further moments of OnLoop.) - Instead of that long "if bitflags & 1 == 1 and..." you could use a short condition, to check the decimal sum of all those bits:

if bitflags == 127 then

It is always easy to calculate a sum like that: highest_bit * 2 - 1 = 64 * 2 - 1 now. (Because we also used Bit0, i.e. 1 now.)

As you can see, we didn't use a bitwise "OR" in this example.
In LUA scripts, we use | symbol (not "i", not "L" letter) instead of the general OR/∨ signs. - The example looks this way with a bitwise "OR":

Code:
-- FILE: Levels\My_level.lua

function OpenDoor()
	bitflags1, bitflags2, bitflags4, bitflags8, bitflags16, bitflags32, bitflags64 = 0, 0, 0, 0, 0, 0, 0
	if GetMoveableByName("switch_type4_17"):GetAnim() == 1 then
		bitflags1 = 1
	end
	if GetMoveableByName("switch_type4_18"):GetAnim() == 1 then
		bitflags2 = 2 
	end
	if GetMoveableByName("switch_type4_19"):GetAnim() == 1 then
		bitflags4 = 4 
	end
	if GetMoveableByName("switch_type4_20"):GetAnim() == 1 then
		bitflags8 = 8 
	end
	if GetMoveableByName("switch_type4_21"):GetAnim() == 1 then
		bitflags16 = 16
	end
	if GetMoveableByName("switch_type4_22"):GetAnim() == 1 then
		bitflags32 = 32
	end
	if GetMoveableByName("switch_type4_23"):GetAnim() == 1 then
		bitflags64 = 64 
	end	
	if bitflags1 | bitflags2 | bitflags4 | bitflags8 | bitflags16 | bitflags32 | bitflags64 == 127 then
		GetMoveableByName("door_type4_16"):Enable()
	else
		GetMoveableByName("door_type4_16"):Disable()
	end
end

LevelFuncs.OnLoad = function() end
LevelFuncs.OnSave = function() end
LevelFuncs.OnStart = function() end
LevelFuncs.OnLoop = function()
	OpenDoor()
end
LevelFuncs.OnEnd = function() end
Or, in this example you need to use five switches to open a door. Three switches are constant, but you can have two-two choices for the further two switches:

Code:
-- FILE: Levels\My_level.lua

function OpenDoor()
	bitflags1, bitflags2, bitflags4, bitflags8, bitflags16 = 0, 0, 0, 0, 0
	if GetMoveableByName("switch_type4_17"):GetAnim() == 1 then
		bitflags1 = 1
	end
	if GetMoveableByName("switch_type4_18"):GetAnim() == 1 then
		bitflags2 = 2 
	end
	if GetMoveableByName("switch_type4_19"):GetAnim() == 1 then
		bitflags4 = 4 
	end
	if GetMoveableByName("switch_type4_20"):GetAnim() == 1 then
		bitflags8 = 8 
	end
	if GetMoveableByName("switch_type4_21"):GetAnim() == 1 then
		bitflags16 = 16
	end
	if GetMoveableByName("switch_type4_22"):GetAnim() == 1 then
		bitflags8 = 8
	end
	if GetMoveableByName("switch_type4_23"):GetAnim() == 1 then
		bitflags16 = 16
	end	
	if bitflags1 | bitflags2 | bitflags4 | bitflags8 | bitflags16 == 31 then
		GetMoveableByName("door_type4_16"):Enable()
	else
		GetMoveableByName("door_type4_16"):Disable()
	end
end

LevelFuncs.OnLoad = function() end
LevelFuncs.OnSave = function() end
LevelFuncs.OnStart = function() end
LevelFuncs.OnLoop = function()
	OpenDoor()
end
LevelFuncs.OnEnd = function() end
As you can see, even two-two switches are able to set Bit3 (8) and Bit4 (16), so if that bit is set by one of the switches, then using the other switch for that bit is useless (but not harmful) to open the door.

Or, if you want that that Lara can use only one-one alternative for the fourth and fifth switches:

Code:
-- FILE: Levels\My_level.lua

function OpenDoor()
	bitflags1, bitflags2, bitflags4, bitflags8, bitflags16 = 0, 0, 0, 0, 0
	if GetMoveableByName("switch_type4_17"):GetAnim() == 1 then
		bitflags1 = 1
	end
	if GetMoveableByName("switch_type4_18"):GetAnim() == 1 then
		bitflags2 = 2 
	end
	if GetMoveableByName("switch_type4_19"):GetAnim() == 1 then
		bitflags4 = 4 
	end
	if GetMoveableByName("switch_type4_20"):GetAnim() == 1 then
		bitflags8 = bitflags8 + 8 
	end
	if GetMoveableByName("switch_type4_21"):GetAnim() == 1 then
		bitflags16 = bitflags16 + 16
	end
	if GetMoveableByName("switch_type4_22"):GetAnim() == 1 then
		bitflags8 = bitflags8 + 8
	end
	if GetMoveableByName("switch_type4_23"):GetAnim() == 1 then
		bitflags16 = bitflags16 + 16
	end	
	if bitflags1 | bitflags2 | bitflags4 | bitflags8 | bitflags16 == 31 then
		GetMoveableByName("door_type4_16"):Enable()
	else
		GetMoveableByName("door_type4_16"):Disable()
	end
end

LevelFuncs.OnLoad = function() end
LevelFuncs.OnSave = function() end
LevelFuncs.OnStart = function() end
LevelFuncs.OnLoop = function()
	OpenDoor()
end
LevelFuncs.OnEnd = function() end
I mean, now if you use both the switches for Bit3 (8) / Bit4 (16), then the bit decimal value will be added twice-twice to the variable, so the condition for the bitwise OR cannot be true, the door won't open. So you can use only one switch for Bit3 and one switch for Bit4, at the same time.

Bitwise "exclusive OR" ("XOR") operation and custom "Midas switches"

There could also be an "XOR" bitwise operation between the same bits of two binary numbers. The operation is marked by XOR word or symbol.
If the bits are the same (both set or both unset), then the result is "unset", but if they are different (one of them is set, the other one is unset), then the result is "set": 0 ⊕ 0 = 0, 0 ⊕ 1 = 1, 1 ⊕ 1 = 0.
For example:

9-----8-----7------6-----5-----4---3---2---1---0 → powers of 2 (bit ID)
512--256--128---64---32---16---8---4---2---1 → decimal bit value
0-----0-----0------1-----0----1----1---0---0---1 → 89
1-----1-----1------0-----0----1----1---0---1---1 → 923
1-----1-----1------1-----0----0----0---0---1---0 → 962

So the result is 89 XOR 923 = 962.

This method should be also familiar, even in the classic Tomb Raider level building: I mean, I am sure you can remember the unforgettable multiswitch setup of Palace Midas. (Or I also discuss it here.)
I mean, the essence of the setup is that you need to use the same switches to open different doors - but each door has its own specific switch combination, using only some of all the switches. If all the bit flags (Bit1, Bit2, Bit3, Bit4, Bit5) are set for a specific door, then that door will open.
Let's see the explanation, with an example:

There are three switches, with these bit flags ticked at their triggers:

5-----4----3---2---1---0 → powers of 2 (bit ID)
32---16---8---4---2---1 → decimal bit value
1----0-----0---1---1---0 → Switch trigger1: 38
0----0-----1---1---0---0 → Switch trigger2: 12
1----1-----1---0---0---0 → Switch trigger3: 56

You want switch1 and switch2 to open together Door A, and switch1 and switch3 to open together Door B.

Let's see first what switch1 and switch2 do together:

5-----4----3---2---1---0 → powers of 2 (bit ID)
32---16---8---4---2---1 → decimal bit value
1----0-----0---1---1---0 → Switch trigger1: 38
0----0-----1---1---0---0 → Switch trigger2: 12
1----0-----1---0---1---0 → Result: 42

If you want Door A to open, then you need the final result to be 62 (11111), so these bit flags need to be set at the door object:

5-----4----3---2---1---0 → powers of 2 (bit ID)
32---16---8---4---2---1 → decimal bit value
1----0-----1---0---1---0 → Switch trigger1+Switch trigger2: 42
0----1-----0---1---0---0 → Door A: 20
1----1-----1---1---1---0 → Result: 62

Now Bit1, Bit2, Bit3, Bit4 and Bit5 are all set, the door will open.

Or aggregated:

5-----4----3---2---1---0 → powers of 2 (bit ID)
32---16---8---4---2---1 → decimal bit value
1----0-----0---1---1---0 → Switch trigger1: 38
0----0-----1---1---0---0 → Switch trigger2: 12
0----1-----0---1---0---0 → Door A: 20
1----1-----1---1---1---0 → Result: 62

So 38 XOR 12 XOR 20 = 62. (So 62 = all the five bits are set.)

And now let's see first what switch1 and switch3 do together:

5-----4----3---2---1---0 → powers of 2 (bit ID)
32---16---8---4---2---1 → decimal bit value
1----0-----0---1---1---0 → Switch trigger1: 38
1----1-----1---0---0---0 → Switch trigger3: 56
0----1-----1---1---1---0 → Result: 30

So Door B should be:

5-----4----3---2---1---0 → powers of 2 (bit ID)
32---16---8---4---2---1 → decimal bit value
0----1-----1---1---1---0 → Switch trigger1+Switch trigger3: 30
1----0-----0---0---0---0 → Door B: 32
1----1-----1---1---1---0 → Result: 62

Or aggregated:

5-----4----3---2---1---0 → powers of 2 (bit ID)
32---16---8---4---2---1 → decimal bit value
1----0-----0---1---1---0 → Switch trigger1: 38
1----1-----1---0---0---0 → Switch trigger3: 56
1----0-----0---0---0---0 → Door B: 32
1----1-----1---1---1---0 → Result: 62

So 38 XOR 56 XOR 32 = 62. (So 62 = all the five bits are set.)

Any other combinations won't open any door - for example:

5-----4----3---2---1---0 → powers of 2 (bit ID)
32---16---8---4---2---1 → decimal bit value
0----0-----1---1---0---0 → Switch trigger2: 12
1----1-----1---0---0---0 → Switch trigger3: 56
1----1-----0---1---0---0 → Result: 52

and then:

5-----4----3---2---1---0 → powers of 2 (bit ID)
32---16---8---4---2---1 → decimal bit value
1----1-----0---1---0---0 → Switch trigger2 + Switch trigger3: 52
0----1-----0---1---0---0 → Door A: 20
1----0-----0---0---0---0 → Result: 32

Or:

5-----4----3---2---1---0 → powers of 2 (bit ID)
32---16---8---4---2---1 → decimal bit value
1----0-----1---0---1---0 → Switch trigger1+Switch trigger2: 42
1----0-----0---0---0---0 → Door B: 32
0----0-----1---0---1---0 → Result: 10

Just as I said above for bitwise OR, this feature is also available in TEN, but you need a full TEN setup for some customization.
Eg. I want a setup, where seven switches open two doors - switch17, 18, 21 and 23 to open door16 and switch 18, 19, 22 and 23, to open door26. (Switch20 is a trick now, it doesn't trigger anything.) And I will use seven bits this time - it has nothing to do with using the same amount of switches now, I could use eg. even twelve bits, to make a really complex code. (Again: I can't set these bits at trigger/object panels now.) - Please note that is also accidental that we use the same amount of switches for each door, I mean you could make a setup eg. even having five switches for one door and nine switches for another one.

6----5-----4----3---2---1---0 → powers of 2 (bit ID)
64---32---16---8---4---2---1 → decimal bit value
1----1----0-----1---0---1---0 → Switch17: 106
0----1----1-----1---0---0---1 → Switch18: 57
1----0----1-----0---0---1---1 → Result17+18: 83
--------------------------------
1----0----0-----1---1---1---0 → Switch21: 78
0----0----1-----1---1---0---1 → Result17+18+21: 29
--------------------------------
1----1----0-----0---0---1---1 → Switch23: 99
1----1----1-----1---1---1---0 → Result17+18+21+23: 126
--------------------------------
0----0----0-----0---0---0---1 → Door16: 1
1----1----1-----1---1---1---1 → Final Result: 127

106 XOR 57 XOR 78 XOR 99 XOR 1 = 127

6----5-----4----3---2---1---0 → powers of 2 (bit ID)
64---32---16---8---4---2---1 → decimal bit value
0----1----1-----1---0---0---1 → Switch18: 57
0----0----1-----0---1---0---1 → Switch19: 21
0----1----0-----1---1---0---0 → Result18+19: 44
--------------------------------
1----1----0-----0---0---0---1 → Switch22: 97
1----0----0-----1---1---0---1 → Result18+19+22: 77
--------------------------------
1----1----0-----0---0---1---1 → Switch23: 99
0----1----0-----1---1---1---0 → Result17+18+21+23: 46
--------------------------------
1----0----1-----0---0---0---1 → Door26: 81
1----1----1-----1---1---1---1 → Final Result: 127

57 XOR 21 XOR 97 XOR 99 XOR 81 = 127

In LUA scripts, we use ~ symbol instead of the general XOR/⊕ signs (the 1 and 81 values, which should be adjusted at the doors, are constant numbers in this setup):

Code:
-- FILE: Levels\My_level.lua

function OpenDoors()
	switch17, switch18, switch19, switch21, switch22, switch23 = 0, 0, 0, 0, 0, 0
	if GetMoveableByName("switch_type4_17"):GetAnim() == 1 then
		switch17 = 106
	end
	if GetMoveableByName("switch_type4_18"):GetAnim() == 1 then
		switch18 = 57
	end
	if GetMoveableByName("switch_type4_19"):GetAnim() == 1 then
		switch19 = 21 
	end
	if GetMoveableByName("switch_type4_21"):GetAnim() == 1 then
		switch21 = 78
	end
	if GetMoveableByName("switch_type4_22"):GetAnim() == 1 then
		switch22 = 97
	end
	if GetMoveableByName("switch_type4_23"):GetAnim() == 1 then
		switch23 = 99
	end	
	if switch17 ~ switch18 ~ switch19 ~ switch21 ~ switch22 ~ switch23 ~ 1 == 127 then
		GetMoveableByName("door_type4_16"):Enable()
	else
		GetMoveableByName("door_type4_16"):Disable()
	end
	if switch17 ~ switch18 ~ switch19 ~ switch21 ~ switch22 ~ switch23 ~ 81 == 127 then
		GetMoveableByName("door_type4_26"):Enable()
	else
		GetMoveableByName("door_type4_26"):Disable()
	end
end

LevelFuncs.OnLoad = function() end
LevelFuncs.OnSave = function() end
LevelFuncs.OnStart = function() end
LevelFuncs.OnLoop = function()
	OpenDoors()
end
LevelFuncs.OnEnd = function() end
Why? Because something XOR decimal 0 = something, i.e.:

"switch17 ~ switch18 ~ switch19 ~ switch21 ~ switch22 ~ switch23 ~ 1" for door16:

106 XOR 57 XOR 0 XOR 78 XOR 0 XOR 99 XOR 1 = 127

and "switch17 ~ switch18 ~ switch19 ~ switch21 ~ switch22 ~ switch23 ~ 81" for door26:

0 XOR 57 XOR 21 XOR 0 XOR 97 XOR 99 XOR 81 = 127

So when a "wrong" switch isn't used, then it won't change the sum value, because its own value is 0. Otherwise it will, having a different own value, so the sum value cannot be 127, even when when all the "right" switches have been used yet.

Don't forget to place one Switch trigger and one fake Trigger on the sector of each trigger, just like in the bitwise "OR" example. (Including the sector of that tricky switch, which also needs to work as if that did something.)

The function with binary numbers - use them if you wish:

Code:
function OpenDoors()
	switch17, switch18, switch19, switch21, switch22, switch23 = 0, 0, 0, 0, 0, 0
	if GetMoveableByName("switch_type4_17"):GetAnim() == 1 then
		switch17 = tonumber("1101010", 2)
	end
	if GetMoveableByName("switch_type4_18"):GetAnim() == 1 then
		switch18 = tonumber("0111001", 2)
	end
	if GetMoveableByName("switch_type4_19"):GetAnim() == 1 then
		switch19 = tonumber("0010101", 2)
	end
	if GetMoveableByName("switch_type4_21"):GetAnim() == 1 then
		switch21 = tonumber("1001110", 2)
	end
	if GetMoveableByName("switch_type4_22"):GetAnim() == 1 then
		switch22 = tonumber("1100001", 2)
	end
	if GetMoveableByName("switch_type4_23"):GetAnim() == 1 then
		switch23 = tonumber("1100011", 2)
	end	
	if switch17 ~ switch18 ~ switch19 ~ switch21 ~ switch22 ~ switch23 ~ tonumber("0000001", 2) == tonumber("1111111", 2) then
		GetMoveableByName("door_type4_16"):Enable()
	else
		GetMoveableByName("door_type4_16"):Disable()
	end
	if switch17 ~ switch18 ~ switch19 ~ switch21 ~ switch22 ~ switch23 ~ tonumber("1010001", 2) == tonumber("1111111", 2) then
		GetMoveableByName("door_type4_26"):Enable()
	else
		GetMoveableByName("door_type4_26"):Disable()
	end
end
Unary bitwise "NOT" operation

I think it is enough if you simply keep this formula in mind:

NOT X = - X - 1

So eg. NOT 89 = -90.

Just at XOR operations, the sign of the operation is ~ in LUA - but this time not between two numbers, but only before one number: ~ 89. (Not really a useful operation, for a casual builder, like you, though.)

There also are "not AND" ("NAND") and "not OR" ("NOR") bitwise operations, but they are not really important to you in TEN scripting. Besides, they don't have initial LUA functions.

Binary shifting

At binary shifting each bit of the binary number moves to left or right, with scripting like this:

A << X → The bits of Number A move left with X position
B >> Y → The bits of Number B move right with Y position

For example:

5-----4----3---2---1---0 → powers of 2 (bit ID)
32---16---8---4---2---1 → decimal bit value
0----0-----1---0---1---0 → The number which will move left: 10
0----1-----0---1---0---0 → 10 << 1 = 20
1----0-----1---0---0---0 → 10 << 2 = 40

5-----4----3---2---1---0 → powers of 2 (bit ID)
32---16---8---4---2---1 → decimal bit value
0----0-----1---0---1---0 → The number which will move right: 10
0----0-----0---1---0---1 → 10 >> 1 = 5
0----0-----0---0---1---0 → 10 >> 2 = 2

As you can see:
  • When shifting left:

    • That is actually a multiplication by 2.
    • New 0 binary bit values come from the right.

  • When shifting right:

    • That is actually a division by 2. (Without the remainder. See eg. 5/2 = 2 now, not 2.5.)
    • The bit will be removed, just moved over Bit0 (to the Bit"-1" position) on the right.
I don't think that binary shifting is usually useful for a casual builder. I mean, as I said, it is actually a very simple multiplication/division.
Many times this operation could be useful for color codes. A color code is a really huge number, and you need shifts to split it into red, green, blue components. Or the opposite: to merge the components into the huge code. - However, in TEN you could already see Color(R, G, B) preset TEN function to define a color with its components, you don't need a workaround for that with binary shifting.

Last edited by AkyV; 12-11-24 at 12:08.
AkyV is online now  
Old 09-10-24, 19:40   #7
AkyV
Moderator
 
Joined: Dec 2011
Posts: 5,074
Default

6. String operations

Let's sum up that how many things you have already learnt so far about texts (I mean: strings) in my tutorials about scripting techniques:
  • Type texts in "" quotation marks.
  • We shortly introduced, but used many times ShowString, DisplayString TEN preset functions, to print things on the screen, for testing reasons.
  • Use tostring function to convert numbers (or other non-textual values) into text.
  • Use tonumber, math.tointeger functions to convert text/binary numbers/hexadecimal numbers into decimal numbers.
  • Use .. sign to link texts to each other.
  • Printing table contents.
But you can do even numerous other operations on texts - only the initial LUA ones will be introduced now, TEN specific features will be discussed in another tutorial:

Quotation

Instead of double quotation marks, you can also place the text even between single quotation marks or double square brackets - so these are all the same:

"Hello, Lara!"
'Hello, Lara!'
[[Hello, Lara!]]


The thing that you can have more than one possibility, could be really useful if you'd like to print quotation marks in that text. In this case use a mixed solution: so one of "", '' or [[]] signs will signal it as a text, while the other symbol, inside this text, will be printed as a quotation mark - for example:

'Zip said: "Hello, Lara!"'
"Zip said: 'Hello, Lara!'"


The version with brackets is a splendid solution when you have no choice. I mean, when you need to type both quotation marks and apostrophes in the text:

[[Lara said: "it's splendid!"]]

If you would like to use the same quotation marks, both to signal the text and to print these marks, then type the printable ones with a backslash:
  • " will be printed where you type \'' inside the text, or
  • ' will be printed where you type \' inside the text.
"Lara said: \''it's splendid!\''"
'Lara said: "it\'s splendid!"'


(That should be not '' - i.e. twice a single quote - but " - i.e. once a double quote - where you can see \'' - backslash+2 x single quote -, but if I type " - double quote - there, then the forum engine will remove the backslash.)

Cursor position

You could change the cursor position while the text is being printed (don't work in texts signaled by double square brackets):
  • Let's suppose you have two lines on the screen, and each line has one left side text and one right side text. You want to arrange these sides nicely, so the left side texts will show up in a left column, and the right side texts will show up in a right column.
    You can type spaces between the two sides, to arrange them into two columns, but the result will be more or less ugly. Instead use horizontal tabulations, which will make nice columns. - Why? Because tabulations move the cursor (rightwards) into an exactly defined position, which position will be surely the same in both lines. (Having the same distance between two tabulated positions next to each other.)
    The horizontal tabulation is marked by \t sign - for example:

    Code:
    LevelFuncs.OnLoop = function()
    	ShowString(DisplayString("Zip said:\t\t\t\t'Hello, Winston!'", 250, 250, Color(255,0,0)), 1/30)
    	ShowString(DisplayString('Winston said:\t"Hello, Zip!"', 250, 350, Color(255,0,0)), 1/30)
    end
    Click image for larger version

Name:	100.jpg
Views:	25
Size:	61.0 KB
ID:	7887

    The lines, i.e. their left parts are printed from the same (250) scripted position, so the left parts are already nicely arranged into a column.
    The lower line has a longer left part. I used one tabulation (\t) there, which moves the cursor to the next tabulated position after the end of the left part, to start printing the right part there.
    The upper line has a shorter left part. Using one tabulation makes a tabulated position which is still far from the tabulated position of the lower line. A second and a third tabulation is still not enough to have the cursor at the tabulated position of the lower line. The fourth tabulation (\t\t\t\t) at last moves the cursor into the position which is the same as the tabulated position of the lower line. Now the game can start printing the right part. Both the right parts are printed from the same position, so the right parts are nicely arranged into a second (right) column. - I drew blue lines so you can see the approximate positions of the tabulations.
  • If you want to break the current text into more than one line, then use \n sign:

    Code:
    LevelFuncs.OnLoop = function()
    	ShowString(DisplayString("Zip said:\n'Hello, Winston!'", 250, 250, Color(255,0,0)), 1/30)
    	ShowString(DisplayString('Winston said:\n"Hello,\nZip!"', 250, 350, Color(255,0,0)), 1/30)
    end
    So the cursor will continue printing the text in a new line (with automatic vertical distance between the lines), when the game reads \n in the text. The starting position of the new line is the same one where the original line was started.

    Click image for larger version

Name:	101.jpg
Views:	21
Size:	33.0 KB
ID:	7900
Lower case, upper case
  • This function will turn all the lower case letters of a text into upper case letters:

    string.upper("helLO!") → HELLO!

  • This function will turn all the upper case letters of a text into lower case letters:

    string.lower("helLO!") → hello!
Length

This function tells that there are how many characters in a text (including spaces, exclamation marks etc.):

string.len("Hello, Lara!") → 12

Character position

This function will search for the coordinates of a part in some text:

local a, b = string.find("Hello, Lara!", "Hello,") → 1, 6

So the "Hello," part in "Hello, Lara!" text starts at the first character of the text, and it ends at the sixth character of the text.

You also need to know:
  • If there are more matches than one, then only the first match will be detected. Use a third argument if you want to know the coordinates of not the first match:

    local a, b = string.find("Lara Lara Lara", "Lara", 5) → 6, 9

    Now you start searching at the fifth character of the text, which is the first empty space, after the first "Lara". So 6 and 9 are the coordinates of the second "Lara".
  • If there is no match, then the variable values are nil.
  • LUA calls these characters "magic characters": ( ) . % + - * ? [ ^ $. Because they can have special tasks (see more below about the patterns), so they basically cannot be considered in searches as printable characters. - For example in this case we can't search for "Lara?", we will search only for "Lara":

    local a, b = string.find("Lara? Lara? Lara?", "Lara?", 5) → 7, 10

    To consider a magic character as a printable one, you have two choices:

    • There should be one more argument in the function, with "true" value:

      local a, b = string.find("Lara? Lara? Lara?", "Lara?", 5, true) → 7, 11

      If you do not want the third argument, but you want that "true" after it, then naturally just type 1 as the third argument.
    • Type the magic character with a "%" - but you can use it for any non-alphanumeric character (like eg. !) if you are unsure if that is magic:

      local a, b = string.find("Lara?! Lara?! Lara?!", "Lara%?%!", 5) → 8, 13

      Yes, if you want to search for a printable "%", then you need to type %%.
Character presence

This function will search for the presence of a part in some text:

local a = "Hello, Lara!"
string.match(a, "Hello")
→ Hello

So the "Hello" part is presented in "Hello, Lara!" text. That is why the function result is this part itself.
If there is no match, then the function result is nil.
So you can do a condition here: "if the function result is not nil, then print the function result". So if local a is "Hello, Lara!", then "Hello" will be printed. But if you change the variable value meanwhile, eg. to "Good bye, Lara!", then nothing will be printed.

You also need to know:
  • If there are more matches than one, then only the first match will be detected. But you can set a third argument to start searching from a specific character position:

    local a = "Hello, Lara!"
    string.match(a, "Hello", 7)
    → nil

    Nil, because you start searching from the seventh character, and you cannot find "Hello" after that character in this text.
  • Don't forget to search for the magic characters in the proper way:

    local a = "Hello? Lara?"
    string.match(a, "Hello?")
    → Hello

    local a = "Hello? Lara?"
    string.match(a, "Hello%?")
    → Hello?

  • A special version of this is string.gmatch function, which works in a special loop, to list all the matches - a similar way as a pairs or ipairs:

    Code:
    local a = "Laraaa Lara Lura LaraLara Lora"
    local pos = 0
    for part in string.gmatch(a, "Lara") do
       ShowString(DisplayString(part, 250, 350 + pos, Color(255,0,0)), 1/30)
       pos = pos + 100
    end

    Lara
    Lara
    Lara
    Lara

    So this time the four matches for "Lara" will be printed below each other, on the screen. (You still need to be careful with the magic characters.) - Yes, "Laraaa" is also a match, for the part of "Lara", because the unmatching part of "aa" at the end is ignored. And "LaraLara" will be split into two matches.

    You also need to know:

    • Previously known as string.gfind.
    • Be careful: this time "no match" seemingly will be considered not "nil", but nothing.
    • There should also be a third argument here, to start searching from a specific character position, but I don't think that works.
Substring

A substring is a text separated from some bigger text. This function will do substrings:

local a = "Hello, Lara!"
string.sub(a, 1, 6)
→ Hello,
string.sub(a, 8) → Lara!

The further arguments after the text argument:
  • The second argument is the position in the original text, where the substring starts.
  • The third argument is the position in the original text, where the substring ends. If there is no third argument, then the substring ends where the original text ends.
So "1, 6" means the first and the sixth character of "Hello, Lara!", while "8" means the eighth character of "Hello, Lara!". (The seventh character is the empty space.)

Substitution

The function of string.gsub will swap a part of some text for other characters:

ShowString(DisplayString(string.gsub("Hello, Winston!", "Winston!", "Zip!"), 250, 350, Color(255,0,0)), 1/30) → Hello, Zip!

You also need to know:
  • If there is no match then nothing will be substituted.
  • Naturally more matches will do more substitutions:

    string.gsub("Hello, Winston! WinstWinston!", "Winston!", "Zip!") → Hello, Zip! WinstZip!

    But you can use an optional fourth argument, to narrow down the amount of substitutions (counted obviuously from the first match):

    string.gsub("Hello, Winston! WinstWinston!", "Winston!", "Zip!", 1) → Hello, Zip! WinstWinston!

  • You can use a second result for the function, with multiple assignment. This second result is a number, which is the amount of the substituted matches.
  • Don't forget about the magic characters:

    string.gsub("Hello, Winston?", "Winston?", "Zip?") → Hello, Zip?? (Because we search for "Winston", not "Winston?", so "Winston" will be swapped for "Zip?". I.e. the "?" after "Winston will be kept.)

    string.gsub("Hello, Winston?", "Winston%?", "Zip?") → Hello, Zip?
Multiple printing

The function of string.rep will repeat the text the required times:

ShowString(DisplayString(string.rep("Lara", 3), 250, 350, Color(255,0,0)), 1/30) → LaraLaraLara

Use one more argument for a separator between them:

string.rep("Lara", 3, " ") → Lara Lara Lara

Printing backwards

This function will turn the characters of a text into the opposite order, so they will be printed backwards:

string.reverse("Hello, Lara!") → !araL ,olleH

ASCII codes

For any reason, you can call a character even via its ASCII (decimal) code, using this function (except ASCII extended characters):

string.char(97) → a

ShowString(DisplayString(string.char(76) .. string.char(97) .. string.char(114) .. string.char(97), 250, 350, Color(255,0,0)), 1/30) → Lara

Alternatively, you can also type a backslash, to call that code in a text:

"L\97r\97" → Lara

This function does the opposite, telling the ASCII code of a character:

string.byte("a") → 97

Or if you search for a code in a longer text, then use a second argument, which tells the position of the required character in this text (otherwise it will be the code of the first character):

string.byte("Lara", 4) → 97

Please note that in this tutorial we don't examine how you can print ASCII extended characters or Unicode characters in TEN.

Inserting values in a string

You already know tostring function to convert a number into some text. But you can be more specific to insert values in strings, if you use string.format function - for example:

local sum = 30
string.format("%i plus %i is %i", 10, 20, sum)
→ 10 plus 20 is 30

So the function could have numerous arguments. The first is the input text with one or more % directives, and the others are values to be inserted in the text. The function result is almost the input text itself - except that you will see the values-to-be-inserted in the output text where you can see the directives in the input text: the first value-to-be-inserted will be written where you can see the first directive, the second value-to-be-inserted will be written where you can see the second directive etc.
The letter code in the directive tells that how the directive will recognize the input value. For example, %i means that value will be used as a (decimal) integer number, even if the value is defined in a different form. So either you really type 20 or its hexadecimal version (0x14), the value will be inserted (and printed) as 20.

The directives are:
  • %c: the input is an ASCII code, which will be inserted as the character that that code means.
  • %d or %i: the input is an integer number or a hexadecimal number (either positive or negative), inserted as an integer number.
  • %f: the input is an integer number, a floating number or a hexadecimal number (either positive or negative), inserted as a floating number.
    Always six digits will be printed after the decimal point, so eg. 12 → 12.000000, 12.56 → 12.560000.
    If those digits are more than six in the input, then the printed value will be rounded up (5 or above) or down (4 or below), eg. 12.123456789 → 12.123457.
  • %g: the input is an integer number, a floating number or a hexadecimal number (either positive or negative). Integer or hexadecimal values will be inserted as integer numbers. Floating numbers will be inserted as floating numbers (with as many digits after the decimal point as it is typed in the input).
    The decimal form of the input number cannot have more digits than six, aggregated before and after the decimal point: 123456, 12345.6, 1234.56, 123.456, 12.3456, 1.23456. Except: plus one more, if the value is between 0 and 1: 0.123456. (The same if the value is negative.)
    Otherwise:

    • The amount of digits after the decimal point will be reduced inserted to this limit, and the number will be rounded up or down: 123.456789 → 123.457. Or 123456.7 → 123457.
    • In the case of the input having more than six digits before the decimal point, the inserted number will be transformed into the form of the so-called "scientific notation" (eg. 6.871593e+10).

  • %x or %X: the input is an integer number or a hexadecimal number, inserted as a hexadecimal number (without signaling with any prefix that it is inserted as hexadecimal). (Use it as a positive value. It could be even negative, but perhaps in that case it is not easy to understand for a casual level builder that what exactly the inserted value means.)
    The difference between %x and %X is the letters in the inserted hexadecimal number will be printed as lower case letters when it is %x, and they will be printed as upper case letters when it is %X.
  • %s always inserts a string, like:

    local name = "Natla"
    string.format("%s and Pierre", name)
    → Natla and Pierre
You can optionally use flags and other values for the directives:
  • The - flag will count (maximum 99) character positions from the point where the game started printing this inserted value. To define the required amount of these positions, you need to type the "width" value to the flag. - For example:

    string.format("%-15s and Pierre", "Natla")

    "%-15s" says there is a string is inserted now (%s), with 15 character positions (-15). Which means the game counts 15 character positions from the left side of N in Natla, before starting printing " and Pierre". (I.e. there will be numerous empty spaces between Natla and and words.)

    Click image for larger version

Name:	104.jpg
Views:	19
Size:	20.3 KB
ID:	8189
  • If the - flag is missing, then the character positions of the width value will be counted not from the left side of the inserted value, but the right one. - For example:

    string.format("It is %15s and Pierre", "Natla")

    So "%15s" says that the game counts 15 character positions just after printing "It is ". Where those 15 positions end, there will be the right side of the second a in Natla.

    And, in the case of using a positive width value, you could type a 0 before that value. Now the empty spaces made by the directive will be printed as zeros:

    string.format("It is %015s and Pierre", "Natla") → It is 0000000000Natla and Pierre (because Natla's five characters + ten zeros = 15 character positions) - naturally it is useful with number inputs, but temporarily you can also use it with strings, to see where those empty positions are

  • The "precision" value will manipulate the character amount (maximum to 99). Just like in the case of the width value, you also need to type the precision value between the % sign of the directive and the directive letter code. But this time type the number after a dot, like: %.3f.
    The precision value work differently with different directives:

    • Using %d, %i, %x or %X directive:
      The number after the dot must be bigger than the digit amount of the inserted value. In that case the value will be printed with as many digits as the precision says, printing meaningless zeros where you don't need digits:

      string.format("%.8d plus 200", 150) → 00000150 plus 200
      string.format("%.3x plus 200", 150) → 096 plus 200

    • Using %f directive:
      This form has six digits after the decimal point. But this amount will be changed with the precision value: the value after the dot will be the new value of the digits after the decimal point. If some digits need to be cut off, then the value will be rounded up or down:

      string.format("%.3f and 15.12", 123.456789) → 123.457 and 15.12
      string.format("%.8f and 15.12", 123.456789) → 123.45678900 and 15.12
      string.format("%.0f and 15.12", 123.456789) → 123 and 15.12

      You don't need to type that 0 now everyway: %.0f = %.f
    • Using %s directive:
      The number after the dot must be less than the character amount of the inserted string. In that case only the first characters of that string will be printed - as many characters as the precision says:

      string.format("%.4s and Zip", "Lara Croft") → Lara and Zip

    • If you also need a width value now, then print the width value before the dot:

      string.format("%-17.8d plus 200", 150)
      string.format("%20.3f and 15.12", 123.456789)
      string.format("%-10.4s and Zip", "Lara Croft")

  • You can also use + flag in a directive.
    If the printed value is either an integer or a floating value, which can be either positive or negative, but it is currently just positive, then it will be printed with the + sign, if you want - just use this flag:

    string.format("%+d plus 200", 150)

    In this example "15 width" formatting will be used for the inserted integer (like it is only %15d), but that will be written as +150 this time:

    string.format("%+15d plus 200", 150)

    Its meaning will be the same even with other flags (on any side of that other flag) - eg. if it already has - flag for "-15 width":

    string.format("%+-15d plus 200", 150)
    string.format("%-+15d plus 200", 150)

    The + flag doesn't have an effect in negative input numbers.
    (Naturally you can also use a + sign before the directive, as a + sign to that positive number - but only if the directive won't move the inserted number away from that sign. Eg. it is a good solution: +%d. But it is not: +%15d.)
  • # flag could have different meanings:

    • Using %x directive: the hexadecimal output will be printed with a 0x prefix: eg. %#x.
    • Using %X directive: the hexadecimal output will be printed with a 0X prefix: eg. %#X.
    • Using %f directive: the decimal point will be printed even if there is no digit after that (i.e. if precision = 0):

      string.format("%#.f and 15.12", 123.456789) → 123. and 15.12

    • Using %g directive: the six digits will be printed even if you don't need them:

      string.format("%g", 150.1) → 150.1
      string.format("%#g", .150.1) → 150.100
Complex examples:

ShowString(DisplayString(string.format("%+014.5f", 127.68), 250, 350, Color(255,0,0)), 1/30) → +0000127.68000

As you can see, even if the + sign of the number (just like the - sign, anyway) is counted in the 14 characters of the width value, the meaningless zeros are (naturally) printed after the + sign. - But if that 0 isn't in the code (%+14.5f), then the empty spaces instead of the zeros are naturally printed before the + (or -) sign.

string.format("%#+-8g", 76.4) → +76.4000

As I said above at +-/-+, the order of the flags could be anything, so that #+- could be even #-+, -#+, -+#, +-#, +#-.

string.format("%084.6g", 79) → 000079

The precision makes six digits, including four zeros before the number. That is why that 0 in the code doesn't have an effect now, further zeros won't be printed, instead empty spaces will be printed.

string.format("%#.10x", 0xFA73D8) → 0x0000fa73d8

As you can see, the 0x prefix isn't included in the ten digits defined by the precision.

Patterns

A pattern could be, after all, any key which you use to search for matches in a text.
For example, in the "for part in string.gmatch(a, "Lara") do" example above "Lara" word is a pattern.
Or, in the "string.gsub("Hello, Winston?", "Winston%?", "Zip?")" example above "Winston%?" is also a pattern.
But there are not only "normal" patterns like that, but even special patterns, which are not printable characters, because they refer to character groups.
For example there is a special pattern which refers to the character group of letters.

Just like when you signal an inserting directive, or a magic character as a printable one, special patterns are basically also signaled with % sign in LUA.
Eg. "%a" pattern is for the letter characters. Or "%s" pattern is for the empty space characters.

Each appearance of a special pattern is only for one character. For example, the matches for %a could be a, A, b, B, c, C etc. So the text of "aBc" could have even three matches for %a.
It depends on the searching type that how many matches a pattern will have in a text - for example:
  • string.find function detects only the first match - counted from the first character of the text, or a specific one you choose,
  • string.gsub function will take as many substitutions as many matches it will find in the text.
Patterns are always used with string.gsub, string.find, string.gmatch, string.match functions. - Examples for special patterns:
  • All the letters of "Hello, Winston!" text of variable a will be swapped for question marks - and then you copy the result into variable a, overwriting the original text there:

    local a = "Hello, Winston!"
    a = string.gsub(a, "%a", "?")
    → ?????, ???????!

  • The first empty space in "Lara Lara Lara" is at the fifth character, and only one character long:

    local a, b = string.find("Lara Lara Lara", "%s") → 5, 5

  • The first letter in "Lara Lara Lara" is an "L":

    string.match("Lara Lara Lara", "%a") → L

  • The four letters of "Lara!!!" are printed one by one, below each other (i.e. this is a good choice for vertical printing):

    Code:
    local a = "Lara!!!"
    local pos = 0
    for part in string.gmatch(a, "%a") do
       ShowString(DisplayString(part, 250, 350 + pos, Color(255,0,0)), 1/30)
       pos = pos + 100
    end

    L
    a
    r
    a
Note the quotation marks. I mean, the special patterns are handled as texts.

The special patterns are:
  • .: any character (including the non-printable ones, like \n) - the only special pattern without % sign
  • %a: any letter
  • %c: any non-printable character (so-called "control characters") - please note that these signs (eg. \n) are handled as only one character
  • %d: any digit character: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
  • %g: any printable character (except: space)
  • %l (lower case L): any lower case letter
  • %p: any punctuation character: . , ? ! : ; @ [ ] _ { } ~
  • %s: any non-printable (control) or space character
  • %u: any upper case letter
  • %w: any alphanumeric character: lower/upper case letters and digits
  • %x: any character which can be used for hexadecimal numbers: a-f, A-F, 0-9
  • The pattern code in the upper case version will mean the opposite. So eg. if %a is for the letter characters, then %A is for the non-letter characters (including the non-printable ones, like \n). (%G is the same like %s, and %S is the same like %g.)
Complex patterns are when the pattern is more than a simple or a special pattern, because a complex pattern is mostly made of at least two "subpatterns", where each subpattern is a simple/special pattern:
  • If each appearance of a special pattern is only for one character, then repeating a special pattern, one after the other, means you look for the matches having as many characters as the amount of the special pattern.
    For example "%d%d%d" means you look for a number having three digits.
    Eg. string.gmatch function will give two solutions for this, on the text of "Winston12345 678": 123 and 678.
    The first match it can find is the first three numerical characters of Winston12345: 123. 234 and 345 cannot be matches now, because a part of a previous match cannot be repeated. Which means the second match could be the next case when you can find at least three other connected digits after the digit of 3: this is 678.
  • You can also mix special patterns by each other.
    For example, "%d%a%d" means you will get matches for character chains which have three characters, in this order: digit, letter, digit.
    Moreover, you can mix normal and special patterns by each other.
    For example, string.match looks for matches with this complex pattern: "W%ansto%l". So the second character could be any letter, and the last character could be any lower case letter. So now not only Winston is a match, but also eg. WInston, Wunston, WXnston, WXnstoq, WHnstoe etc. But matches cannot be eg. Wimston, W7nston, WinstOb, WinstoN, Wpnsto~ etc.
  • In a pattern set you type more than one pattern in square brackets. What matters is if any of the patterns in the set is true, then the match is detected.
    For example, in the complex pattern of "W[iI]nston" Winston or WInston are both matches. (So each printable character is an independent normal pattern now.)
    Or, in the complex pattern of "W[%a%d]nston" any text is a match when the second character is any letter or any number: Winston, Wanston, Wbnston, W1nston, W2nston etc.
    Or, there could be even really complex pattern sets. Eg. "[%u%.!1-3]" means there is a match if the character is an upper case letter, a dot, an exclamation mark or a digit between 1 and 3. (I mean, dot is a magic character, for the pattern of all the characters, so if you look for the dots themselves, then you can do it with the %. sign.)
    Yes, the "X-Y" formula works now, you don't need to type all the characters. But, if you wish, feel free to type 123 now instead of 1-3. However, keep in mind that "X-Y" formula is available only for pattern sets.
  • Use ^ sign for the opposite values of the patterns in the set (always as the first character inside the brackets).
    For example "[^%u%.!1-3]" means any character which is not an upper case letter, not a dot, not an exclamation mark or not a digit between 1 and 3.
  • ^ magic sign can be used not only in pattern sets - but still typed only as first characters.
    In these cases the sign says that you cannot start the string with some unmatching part which will be ignored - because we cannot ignore that now.
    For example, if the text you examine is "Lara Croft", then "Croft" pattern will find Croft match, ignoring the "Lara " part of the text. But "^Croft" pattern cannot ignore "Lara " part now, it can find Croft match only if there is nothing typed before C letter. So eg. "Croft" or "Croft Lara" etc. texts are useable now.
    The sign of $ has a similar meaning - but signaling the last character now. Eg. "Lara$" pattern has no Lara match in "Lara Croft" text, only if that text is "Lara", "Croft Lara" etc.
    Naturally eg. "^Lara$" pattern means that we accept only "Lara" string to get Lara match.
    (So ^ sign says: "the string starts here" and $ says: "the string ends here".)
  • Pattern modifiers (only for the pattern/character typed just exactly before this sign):

    • Use a + sign in the pattern when you'd like to detect the match, with any amount of the pattern.
      For example, for the complex pattern of "Wi+nston", these are all matches: Winston, Wiinston, Wiiinston, Wiiiinston, Wiiiiinston etc.
      Or, for example, for the complex pattern of "W%a+nston", these are all matches: Winston, Waanston, Wbbbnston, Wccccnston, Wdefghnston etc.
      Or, in this case the match is detected with any amount of either letters and/or digits, between W and n: "W[%a%d]+nston". For example: W1a2b3cnston.
      Or, see the example above when you swap all the letters of "Hello, Winston!" text of variable a for question marks. If you type a + after %a, then you won't search for "any letter" (%a), but you will search for "all the letters in a row" (%a+). There are only letter characters in this example in each word, so "all the letters in a row" means one word now. The example has two words, so the first match is the first word (Hello), and the second match is the second word (Winston). Each match is still swapped for a question mark, so now both Hello and Winston will be swapped for a ?:

      local a = "Hello, Winston!"
      a = string.gsub(a, "%a+", "?")
      → ?, ?!

    • The sign of * will detect the same matches as + sign. - Except it also signals a match when the pattern is not detected at all.
      So eg. in the case of "Wi*nston" Wnston will be also a match.
    • When you use a - sign, that many times will do the same what * sign does, too.
      The difference shows up only under some special circumstances. For example, we run a string.match function on "XyyyXyyyZyyyZ" text. Both - and * will look for any letters between X and Z letters, with these complex patterns: "X%a*Z", "X%a-Z". Both of them will find the left X immediately for the start of the match.
      The difference is where the match ends. I mean, if you consider the left Z as closing Z, then "any letters" are yyyXyyy. But if you consider the right Z as closing Z, then "any letters" are yyyXyyyZyyy. - So the difference is:

      • * (and +) are always looking for a match in the longest chain: XyyyXyyyZyyyZ.
      • - is always looking for a match in the shortest chain: XyyyXyyyZ.

    • The sign of ? means an optional character.
      So "Winston?" will signal matches if the text is either Winston or Winsto. (Eg. for Winstob text it is also a match, but the match naturally ignores that unmatching last character, so the match is still Winsto.)
      Or, "Winsto?n" will signal matches if the text is either Winston or Winstn.
      Or an interesting example: if the pattern is "[+-]?%d+", then you look for matches which are numbers, having any digits, optionally having + or - sign. So eg. these are all matches now: -1, 23, +456.

  • The sign of %bxy always says: "all the contents between x and y characters, including x and y". - For example:

    local text = "Lara (Zip (Natla)) Winston (Pierre)"
    string.gsub(text, "%b()", "Bartoli")
    → Lara Bartoli Winston Bartoli

    The first match starts at the opening bracket at Z. Its closing pair is the right one of the double brackets, so the first match is (Zip (Natla)), that will be swapped for Bartoli. The second match for Bartoli is (Pierre).
  • The so-called "frontier pattern" (marked by %f) always searches for a position when its contents (a pattern set: typed in square brackets) become true - for example:

    string.match("WINSton and Lara", "%f[%l]%a+") → ton

    Why? Because now %f[%l] searches for the first point now, when the lower case letters become true. It happens just after WINS. The special pattern of %a+ for any letters says that the match ends at the n of WINSton, because that is the last "any letter", followed by a space.
  • Captures mean you type some subpatterns in () brackets, in string.find function, to record some parts of the text in variables (i.e. it is also a way to create substrings), creating custom outputs for the function - for example:

    local _, _, a, b = string.find("Winston, Zip and Lara", "(%a+)%s%a+%s(%a+)") → Zip, Lara

    This time we don't care about the original first and second output of the function (so those are _ dummy variables now), but we create a third and a fourth output, recorded (captured) in variable a (the third one) and variable b (the fourth one). The match starts with any amount of any letters, followed by a space ((%a+)%s). This cannot be Winston, because in that case there is a comma between the letters and the space. So it must be Zip. The "any amount of any letters" is in () brackets, so Zip will be the third output, saved in variable a. After that space further letters and one more space ("and ") is coming, and then all the following letters ("Lara") are saved in variable b.
  • You can also use captures to help to create the proper pattern. In this case you will refer to the helping capture with the % sign and a number, from 1 to 9: %1, %2, %3 etc. %1 means the first custom output of the function, %2 means the second custom output of the function etc. - For example:

    local _, _, _, a = string.find("Winston and Lara", "(%s)(.+)%1") → and

    First of all: this time we don't care about not only the original outputs of the function, but even the first custom output. Because that will be used only to help to define the second output (variable a), the only output we are interested in. So we have three dummy variables now.
    The match starts with the first space (%s), between Winston and and words. This space will be captured as the first custom output.
    The match continues with all of any characters (.+), to %1 sign. That sign refers to the first capture, which is a space now. So the second capture is all the characters between two spaces, which is the and word now. (Naturally eg. "%s(%a+)%s" would be also a good pattern now for this.)
  • You will refer from outside to the captures, i.e. not inside the patterns, in the case of string.gsub functions - for example:

    string.gsub("Winston", "(%a)", "%1-%1")

    Now each letter (%a) is a match. That is why each letter will be swapped for "%1-%1". So, when the current character is W, then that will be captured, so "%1-%1" in its case means W-W. Then, when the current character is i, then that will be captured, so "%1-%1" in its case means i-i. - That is why the result of the function now: W-Wi-in-ns-st-to-on-n. (So this time the capture isn't saved in a variable.)
    Or: each letter (%a) is a match, and its following letter is another match. These pairs we have: Wi, ns, to, n. (Yes, n doesn't have a pair now.) - Remember, parts of previous matches cannot be matches again, so we cannot have in, st, on pairs.

    string.gsub("Winston", "(%a)(%a)", "%2%1")

    "%2%1" means the original order of the two matches (%1%2) will be interchanged, in the function result, which will be: iWsnotn.
----------

Notes:
  • Some types of "string" functions seemingly don't need tostring conversion to print their results in ShowString/DisplayString functions, even if that result is not a text (but mostly a number). I mean, the conversion seems automatic now.
    However, be careful with that, because it seems it is not always true. Eg. I experienced I needed tostring to type the default results of string.find function.
  • Notes to backslashes:

    • Theoretically you should use \[ and \] if you'd like to have square brackets inside texts signaled by square brackets, to print them on the screen.
      But seemingly they don't work. Besides, the operation nicely works without backslashes in TEN scripts, eg.:

      [[hello [hello] hello]]

      I discovered bugs only when those brackets are at the edges, making triple brackets, like:

      [[[hello]]]

      But it can be naturally easily solved by signaling the text in another way, like:

      "[hello]"

      Or just simply typing an invisible (but printable) space (the left triple brackets seem okay now):

      [[[hello] ]]

      Please keep in mind that you may encounter a bit different solution in some scripts: i.e. you will find some equals signs between the double brackets - for example:

      [===[hello [hello] hello]===]

      The amount of equals signs could be different, not three, as in this example (but always the same amount on the two sides). For you, the casual builder, it will do the same in TEN scripts, like "[[hello [hello] hello]]", so you don't have any reason to use the solution with equals signs.
      The only reason could be in the case of brackets at the edges - now you don't need that invisible space:

      [===[[hello [hello] hello]]===]

    • As you can see above, a backslash can help you to print characters which couldn't be printed in the regular way, like: ' → \'-, " → \''.
      And this help works even in the cases when another backslash is already used for a special reason. I mean eg. as you can see above, \t will not be printed as \t, because it has a special meaning. - But a helping backslash will print \t, if your purpose to print \t, not using \t for the tabulation.
      So \ + \t = \\t, to print \t.

      And it is naturally true even for the backslash itself. I mean, a standalone backslash won't be printed, it waits for another character. Eg. \ waits for t, to be \t, so that will be a tabulation.
      But, as you could just see, backslash after backslash means you want to print something, so \ + \ = \\, to print \. - For example:

      "backslash quoted: \''\\\''"

      Which is \'' + \\ + \'', so it will look this way on the game screen:

      backslash quoted: "\''

    • You can use the backslashes for quotation marks, when you don't need them, because you signal the text with the other quotation mark: eg. "\'" will print '.
    • As I told above, cursor positioning can't use backslashes for a special reason (\t, \n), if signaling the text in square brackets. - And it is true for every situation, when backslash is used for some special task. (I mean, see eg. the case with ASCII codes above.)
      So all the backslashes will work as a simple backslash now. - For example: [[\t]] will print \t, or [[\']] will print \'.
    • Characters with backslash seemingly should be used the same way in patterns, pattern sets or captures: type \n (again: handled as one character) to search for a new line sign (\n), type \' to search for a printable apostrophe (typed either as ' or as \'), you can even type \97 to search letter a (typed either as a or as \97) etc.

  • Further notes to ASCII characters:

    • Feel free to type meaningless zeros. Eg. \097 instead of \97. (But also in string.char or string.format functions.)
    • Not only printable characters has ASCII codes. Eg. the code of \n is \10.

  • Too long texts won't be broken automatically into more lines, to fit the width of the screen. They will run out of the screen, at the right screen edge. The solution could be to break the text into lines manually in the proper places, using \n signs.
  • You can hit Enter in the text to split the text line into more lines. So the text will be printed this way in the game, you don't need to type \n signs now. But it works only if the text is signaled by double brackets:

    Code:
    ShowString(DisplayString([[Winston said:
    "Hello,
    Zip!"]], 250, 350, Color(255,0,0)), 1/30)
    Or maybe it looks better:

    Code:
    local a = [[Winston said:
    "Hello,
    Zip!"]]
    ShowString(DisplayString(a, 250, 350, Color(255,0,0)), 1/30)
    However, the lower lines this time won't be aligned to the start (left side) of the first line. I.e. they will be printed a bit rightwards from it (but not tabulated):

    Click image for larger version

Name:	102.jpg
Views:	20
Size:	16.6 KB
ID:	7909

    A solution could be to type the first line also in a new line, to also start it from the position of the lower lines (so the starting position of the first line is not the position which is set in DisplayString function):

    Code:
    local a = [[
    Winston said:
    "Hello,
    Zip!"]]
  • You can't type empty lines in a text - either you use the method with \n or hitting Enter.
    This will lead to printing it in a buggy way in the game:

    Code:
    ShowString(DisplayString('Winston said:\n\n"Hello,\n\nZip!"', 250, 350, Color(255,0,0)), 1/30)
    And this time the "entered" empty lines will be ignored in the game:

    Code:
    local a = [[Winston said:
    
    "Hello,
    
    Zip!"]]
    A primitive, but working solution is to print the parts of the text one by one, with the proper vertical distance between the lines:

    Code:
    ShowString(DisplayString("Winston said:", 250, 350, Color(255,0,0)), 1/30)
    ShowString(DisplayString("'Hello,", 250, 450, Color(255,0,0)), 1/30)	
    ShowString(DisplayString("Zip!'", 250, 550, Color(255,0,0)), 1/30)
    Click image for larger version

Name:	103.jpg
Views:	20
Size:	18.0 KB
ID:	7912

  • Useless LUA operations in TEN scripting (without explanation):

    • Backspace (marked by \b sign).
    • Carriage return (marked by \r sign).
    • Form feed (marked by \f sign).
    • Vertical tabulation (marked by \v sign).
    • Bell (marked by \a sign).

  • Empty quotation marks naturally means "nothing" - for example:

    local text = "Winston"
    local print = string.gsub(text, "inst", "")
    ShowString(DisplayString(print, 250, 350, Color(255,0,0)), 1/30)
    → Won

  • I don't think that the functions for packing/unpacking strings are helpful to you now, so I avoid them in this chapter: string.pack, string.packsize, string.unpack.
    The function of string.dump is also not a function for an "advanced" tutorial like this, so that was also skipped.
  • If you use the existing "string" functions cleverly, then you can actually create some new operations. - Eg. swap a character for itself in a text (i.e. actually not doing anything), then use the second output of string.gsub (i.e. the "swap amount") to get to know the amount of that character in the text - for example:

    local _, a = string.gsub("Winston, Lara, Natla, Pierre, Larson", "r", "r") → 4

  • In string.gsub you can swap not only for something typed in quotation marks, but even else:

    • Functions.
      For example, you don't want to use string.upper function to turn all the lowe case letters of "Winston" into upper case letters, because you want to turn only some of them. You can use string.gsub now for the swaps - but you can't use %u in it for this, because %u could be used only as a pattern. But, string.upper is also useable here:

      string.gsub("Winston", "[io]", string.upper) → WInstOn

      Yes, string.upper function has no () now, because the function argument is automatically handled by string.gsub.
      (You can also write custom functions for string.gsub, to tell which will be swapped, but I skip this part now.)
    • Tables.
      This time we check all the letters ("%a+") of the text. If some of them is Winston word, that will be swapped for Natla. And if some of them is Lara word, that will be swapped for Pierre:

      local names = {["Winston"] = "Natla", ["Lara"] = "Pierre"}
      local print = string.gsub("Winston and Lara", "%a+", names)
      → Natla and Pierre

  • Notes to the formatting directives:

    • I didn't mention these formatting directives, because they are not really helpful to a casual level builder and/or perhaps it is not easy to understand them:

      • %o to insert numbers in octal form.
      • %u to insert numbers in unsigned form. (It is not quite an absolute value converter.)
      • %e, %E to insert numbers in the form of a scientific notation.
      • %G after all does the same as %g. It is unnecessary to introduce that tiny difference.
      • %q is a directive with a few additional tasks. Eg. it puts the backslash, instead of you, to printable double quotes.
        (Like string.format("%q", '"hello"') → "\''hello\''")

    • I am not sure that the so-called space flag works here now. - Not really important, though. (That is why not explained.)

  • Further notes to patterns:

    • Sometimes you need \0 special pattern (formerly known as %z) to make the script realize 0 in strings. As far I can see, it is not a problem in the LUA version of TEN, you can avoid using \0 in patterns here.
    • Be careful: special patterns for letters probably will recognize only the English alphabet.
    • You can use pattern sets for the opposite meaning of special patterns.
      For example, it also means "not upper case letters", just like "%U":

      "[^%u]"

    • Some pattern sets can be used instead of special patterns - for example:

      "[A-Z]"

      It also means "upper case letters", just like "%u".

      "[^A-Z]"

      It also means "not upper case letters", just like "%U".
    • I said that you need tou use % if you want to search for magic characters when they are not magic, but printable ones. It is not fully true for ^ and $ characters.
      I mean, you could read above, that what their "magic tasks" are in patterns, at the first/last character positions of the text. If you want to search for ^ or $ printable characters in those positions, then you need % in the pattern, otherwise you don't - because there is no magic tasks for them in other positions.
      So eg. it is a good pattern, to check printable ^ and $:

      "%^Wi$nst^on%$"

    • The function of string.gmatch is also available for captures. I've found it a bit complicated for an "advanced" tutorial, so I miss the explanation now.
    • I am not sure if captures work in pattern sets: [(...)].
      On the other hand, pattern sets will work in captures: ([ab]). - In this case, if you refer later to this capture eg. by %1, then you will refer a or b (because [ab] means a or b). So if a is captured, then %1 will mean a, but if b is captured, then %1 will mean b.

  • When the "main" text of the function is saved in a variable, then you can use "string" functions in a different form. - For example, instead of this:

    local laratext = "Hello, Lara!"
    local laraprint = string.match(laratext, "Hello")
    ShowString(DisplayString(laraprint, 250, 350, Color(255,0,0)), 1/30)


    you can use this:

    local laratext = "Hello, Lara!"
    local laraprint = laratext:match("Hello")
    ShowString(DisplayString(laraprint, 250, 350, Color(255,0,0)), 1/30)


  • Naturally you can use the linking two dots even in the text features I described in this chapter.
    Eg. to extend the output string of the formatting directive:

    local name = "Pierre"
    ShowString(DisplayString(string.format("It is %s and " .. name, "Natla")
    → It is Natla and Pierre

    Or to control the formatting code from outside (for %.4f now):

    local precision = 4
    string.format("%." .. precision .. "f", 2024.1996)
    → 2024.1996

    Or to extend a pattern:

    local text = " and"
    string.match("Winston and Lara", "Winston" .. text)
    → Winston and

    Etc.

Last edited by AkyV; 05-11-24 at 18:29.
AkyV is online now  
Closed Thread

Bookmarks

Thread Tools

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off



All times are GMT. The time now is 16:57.


Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2024, vBulletin Solutions Inc.
Tomb Raider Forums is not owned or operated by CDE Entertainment Ltd.
Lara Croft and Tomb Raider are trademarks of CDE Entertainment Ltd.