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

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

Back To Get Started With Lua Events

Get Started With Lua Events Lesson 4

This lesson is currently under construction.

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.

Exercise

In the Events.lua file for Classic Rome, take all the objects we've given names to thus far, and put them into tables. Use the tables tileAliases , unitAliases, tribeAliases, and cityAliases. Additionally, create a table parameters for numbers such as elephantCampaignCost, and textAliases for strings.

It is a good idea to give parameters names for similar reasons as for why you would like to name objects. There are two big reasons that deserve to be stated explicitly. The first is that it makes tweaking the program much easier. If you use parameter names everywhere, then by changing the definition at one place, you know that everything is changed, without searching all your code. The second is that you don't have to know the final parameter value while programming. As long as you know the name of the parameter, you can program, even if you've forgot or have not decided on the final value for the parameter. This is particularly useful if you are collaborating with someone else. You can program without waiting for confirmation on a value. Similar reasons apply for giving strings names.

Additionally, for everything being re-named, find where it is used, and re-name appropriately. You might find the replace functionality (in the search menu) of Notepad++ useful. If you are not using Notepad++, text editors usually have search and replace functionality.

GetStartedWithLuaLesson4-01-ReplaceMenu.jpg

If you use the search and replace tool in Notepad++, make sure you check the boxes "Match Whole Word Only" and "Match Case" so you don't replace more than you expect.

GetStartedWithLuaLesson4-02-ReplaceWindow.jpg

These are the names that I gave the objects and parameters in question. If you used different names, that is fine, but you will have to take that into account when copying code during this lesson. For Lesson 5, I will provide the events file that I finish with during this lesson.

GetStartedWithLuaLesson4-03-ObjectNames.jpg

Note that I also created a terrainAliases table for terrain type numbers.

When changing names, perhaps you noticed that parameters.campaignCost isn't actually used anywhere (it was replaced with a campaign cost dependent on unit movement points.) You can use the search utility in your text editor to confirm this. You may delete the line if you wish.

Event Examples

Organizing an event file is important, but you're probably reading these lessons in order to have events that are worth organizing. So let us get to some examples.

Capturing An Enemy Unit

In many situations, we would like to be able to "capture" units instead of kill them. In an Age of Sail scenario, for example, we might like to capture a defeated ship instead of sink it. Perhaps we would like to "plunder" a city or unit, and have a "treasure" to transport home to be disbanded. Maybe we would like to capture "slaves".

In our Classic Rome scenario, we will allow military units to "capture" settlers instead of kill them. To do this, we will used the unit killed event trigger of civ.scen.onUnitKilled(function (defender, attacker) -> void) . In our Events.lua file, we already have a function doWhenUnitKilled(loser,winner), to which we will add this event. Note that doWhenUnitKilled uses the names loser and winner instead of defender and attacker.

The first step of this unit killed event is to activate it when a settler is killed. Therefore, we wrap everything in an appropriate if statement:

if loser.type == unitAliases.settler then
end

Since unitAliases.settler is not yet defined, we put this line in the appropriate location:

unitAliases.settler = civ.getUnitType(0)

Now, we create the unit at the location of the winner:


if loser.type == unitAliases.settler then
   local newSettler = civ.createUnit(unitAliases.settler, winner.owner, winner.location)
end

As it stands, this code will create a settler for the winning unit's tribe at that unit's location. (Note that unit.owner is absent from some documentation.) Try it now. Make sure you've remembered to add unitAliases.settler to the unitAliases table.

GetStartedWithLuaLesson4-04-BasicSettlerCapture.jpg

If this is the entire function, we do not need to include the local newSettler = in the code. However, we might like to change the characteristics of the settler that has just been created. The civ.createUnit function sets the home city to the 'nearest' city, the same way a city is assigned to a unit created via the cheat menu. Perhaps we do not like this, and would like for the new settler not to have a home city. Perhaps, also, we wish for the created unit not to have any movement points for this turn, to give the other side a chance to liberate the settlers again. Additionally, we would like the new settler to have only 5 hitpoints remaining after capture.

Use what you have already learned (both in this lesson and previous lessons) and any resources at your disposal (such as TOTPP Lua function reference) to make these three changes to the newly created settler.

Answer:

We add the following three lines:

newSettler.homeCity = nil
newSettler.moveSpent = 255
newSettler.damage = newSettler.type.hitpoints - 5

newSettler.homeCity = nil

Not much to say about this. Nil is how to make a unit not have a home city.

newSettler.moveSpent = 255

This will guarantee that the unit can't move, since 255 is the maximum amount of movement points a unit can have. Another valid option is

newSettler.moveSpent = newSettler.type.move*totpp.movementMultipliers.aggregate

Which will set the expended movement points equal to the maximum movement points the unit has. Beware, however, that this will not work for a ship whose owner has Lighthouse, Magellan's or Nuclear Power, since that unit will have extra movement points that are not caught by unittype.move.

newSettler.damage = newSettler.type.hitpoints - 5

We can't use unit.hitpoints, since that is only a 'get' command. We must set damage, and in order to do that, we must get the hitpoints for the settler type, which we do by newSettler.type.hitpoints, and then subtract 5 from that for the answer.

Bonus points if you put "5" as a parameter to make changing it easier later.

Also, using unitAliases.settler in place of newSettler.type is fine.

GetStartedWithLuaLesson4-05-CaptureUnitAnswer.jpg