Get Started With Lua Events Lesson 4: Examples and More Examples

From Scenario League Wiki
Revision as of 03:53, 11 January 2019 by Prof. Garfield (talk | contribs) (Added to lesson)
Jump to navigationJump to search

Back To Get Started With Lua Events

Get Started With Lua Events Lesson 4

We now know enough about how Lua works to start writing more events. We will again visit the Classic Rome scenario. Find the downoad put link here, whith the event file that I will start the lesson with. Your existing event file from put link to lesson 3 here is probably fine, however, as long as you debugged it properly.

Naming Objects

Why Should You Name Objects?

Most of the functions that are native to TOTPP operate on 'objects' like the unittype object or the city object. In order to get this object in the first place, it is necessary to use functions like civ.getUnitType or civ.getCity. It is recommended that we give objects names and use those names to refer to the object, rather than using civ.getNeededObject(integer) everywhere, or writing "wrapper" functions that can take integers as arguments.

There are several reasons for this recommendation. The first is that it is easier to understand what the code is doing. civ.createUnit(diplomat,romans,rome.location) is much easier to understand than civ.createUnit(civ.getUnitType(50),civ.getTribe(1),civ.getTile(40,26,0)). The second reason is that it is less error prone. Did you notice that the 50th unit in a standard game is not in fact a diplomat, but instead the explorer? Probably not. You are even less likely to realize that Rome is actually located at (40,28). This means you have to do more testing, to make sure every number is correct in your code. If you write the wrong number, the game will probably do something, just not the something you wanted. If you misspell a name, you are likely to get a nil error, and know where to fix the problem. So the second reason is that it is likely to reduce errors, and make it easier to find the errors that do occur.

A second reason to use names instead of integers is that it makes it easier to make changes to your code. Perhaps you originally had a game mechanic that used the meltdown feature of the Nuclear Plant, but then decided that the mechanic didn't work very well after all, and wish to replace it with a regular Power Plant instead. If you used the improvement number 21 in your code, you would have to search your code for every 21, determine if it refers to the Nuclear Plant, then change it to a 19 for the Power Plant instead. With a name, you would simply change NuclearPlant=civ.getImprovement(21) to NuclearPlant=civ.getImprovement(19) and all the events you wrote would continue to work as normal. If you chose to remove the NuclearPlant entirely from the game without replacing it, it is easier to search your code for NuclearPlant, and if you forget something, the game will usually give you an error because a nil value was received instead of an improvement object.

This is something that is actually likely to happen once your event file gets large, especially if you collaborate with others. (And you will have a large event file at some point. Since Lua will let you do almost anything, you will eventually try to do almost everything.) For example, in Over the Reich, unit type 109 was originally some kind of "munition" and was therefore to be deleted every turn. Eventually, we decided that munition wasn't used for very much, and we could use another munition in its place. Later, we used the 109 unit type slot for the "Historic Target" unit which was not supposed to be deleted with the munitions. However, the table governing the unit types to be deleted used integers, so I had to figure out why the unit was being deleted. This particular bug was made more difficult to sort out since sometimes the unit was supposed to be deleted.

Now that we've decided that we should give 'names' to hundreds of objects, we run into a problem. You can only have 200 local variables in a Lua file (module). Usually, it is good programming practice to split your program into multiple files before you have need of 200 local variables (and we'll learn how to do that in a latter lesson), but this doesn't really apply to our situation where we're storing data for our scenario. The solution is to use tables to store these kinds of values. So instead of

settler = civ.getUnitType(0)

We use

unitAliases = {}
unitAliases.settler = civ.getUnitType(0) 

Avoid using a table name that is likely to be a variable name in a function. For example

unit.settler = civ.getUnitType(0)

Would become a problem if we wrote the following loop:

for unit in civ.iterateUnits() do 
   if unit.type == unit.settler then
       civ.ui.text("We found a settler.")
   end
end 

In this case, "unit" is a very natural variable name for this for loop, but declaring a new local "unit" prevents us from accessing the "unit" table that was declared at a higher level. In Over the Reich, objectAliases was chosen as the form for most of these kinds of tables.

How to Name a City Object

For most objects that you would want to name (unit types, technologies, tribes) the corresponding function civ.getObject(id) is easy to use. At the time of writing this guide, it is inadvisible to try to name individual units, since the game will change their id number from time to time (to remove killed units). If it ever does become possible to name individual units, you will probably want to use someone else's code to do it anyway. But what about cities? Unlike unit types, you can't just count entries in the Rules.txt to find the id number.

One method is to use the in game console to find the id number of each city ( civ.getCurrentTile().city.id). This is the method used in Over the Reich, and, has not, thus far, led to any known problems in that scenario. However, there has been a report of a possible bug with this system, which at the time of writing hasn't been investigated further. This being the case, there are at least two other options that can be used (and which might be preferable anyway). The first is

cityAliases.rome = civ.getTile(40,28,0).city

Which is accessed by simply writing cityAliases.rome .

The second option is somewhat similar, except that it uses a function to get the city object at the time it is needed instead of storing the object in a table.

local function getCityAt(x,y,z)
   local function getCity()
       return civ.getTile(x,y,z).city
   end
   return getCity
end

Which is then used in the follwoing way:


local myCity = getCityAt(10,20,0)
-- Other Code
if city == myCity() then
-- code
end

In this method, the function getCityAt(10,20,0) generates another function, which gets the city at (10,20,0) or returns nil if no such city exists, and we call that new function when we want to actually get the city at (10,20,0). This method should not have any problems with cities being created or destroyed, so we'll use it for this tutorial. One disadvantage of the method is that we might not be able to re-use code designed for other kinds of objects. For example, if we have a function inTable(object,table) --> boolean which checks if the object is also a value in the table, we cannot use inTable(city,cityTable) and get true, since cityTable will have functions in it, not city objects. That is another reason for using the function method of getting cities in this tutorial: so we can see how code varies with how we get objects.