Get Started With Lua Events Lesson 4: Examples and More Examples: Difference between revisions

From Scenario League Wiki
Jump to navigationJump to search
(→‎Counting Things: added to lesson)
(→‎Changing A Produced Unit: added to lesson)
Line 396: Line 396:


===Changing A Produced Unit===
===Changing A Produced Unit===
Unfortunatly, at the time of writing, ToTPP doesn't provide the functionality to check or set the production order of a city.  This is inconvenient for a number of reasons, one of them being that we can't check the production of other cities when deciding if a city can build a unit or improvement.  It is therefore possible, in our legion example, that we run into a situation where 2 cities try to produce a legion in a turn, but only one more legion can be "supported." 
One option we have is to simply allow the extra legion to be produced.  Another option, however, is to do something with the on City Production event trigger <code>civ.scen.onCityProduction(function (city, prod) -> void)</code>.
One thing we can do is delete the extra legion, and replace it with a different unit, say an archer.  For example,
<code> local function doOnCityProduction(city,prod)
    if civ.isUnit(prod) then
    if prod.type==unitAliases.legion and countUnits(unitAliases.legion,city.owner) > findNativePopulation(city.owner)/4 then
    local substituteUnit = civ.createUnit(unitAliases.archers,city.owner,city.location)
    substituteUnit.veteran = city:hasImprovement(improvementAliases.barracks)
    civ.deleteUnit(prod)
    end
    end
end
civ.scen.onCityProduction(doOnCityProduction)</code>
[[File:GetStartedWithLuaLesson4-11-ReplaceLegion.jpg]]
Try out this code in ClassicRome, by scheduling more legions to finish construction on a turn than can be supported.
The first thing to mention is the line
<code>substituteUnit.veteran = city:hasImprovement(improvementAliases.barracks)</code>.
As I discovered while writing this lesson, the game does not bestow veteran status on a newly created unit until ''after'' the <code>onCityProduction</code> function is run. 
This means we can't use the veteran status of the legion to apply it to the Archer.  This also means that removing the veteran status bestowed by a barracks will require a bit of work and some functionality we haven't covered yet.
The next thing to note is that I've used the expression <code>findNativePopulation(city.owner)/4</code>, so now there are two places that use '4' for the same purpose.  We should create the parameter <code>parameters.populationPerLegion=4</code> and use that where we need it in place of '4'.  That way, if we want to make a change, we don't have to search for every '4' in the file and determine if it corresponds to a legion cost. Do this now.
We make the check <code>civ.isUnit(prod)</code> for the same reason we check if item is a unit type in our inBuildMenu function.
Another alternative we have to replacing the produced unit is to simply return the shields expended for production.  Unfortunately, the remaining shields in the production box (after adding the city's production for the turn and removing the cost of the unit) are not eliminated until ''after'' the onCityProduction function is executed.  Since we have not enabled production carryover in the rules.txt, giving shields to the city is ineffective.  We'll revisit this in a future lesson.


===Upgrading a Unit Beyond Veteran Status===
===Upgrading a Unit Beyond Veteran Status===

Revision as of 21:15, 11 January 2019

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.

Scroll Down to See Answer












(Hopefully we can get collapsible text at some point)


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

Finding the Nearest City

When we created a settler unit, we decided that it should never be supported, while the default behaviour is for the unit to be supported if it is created near to one of its own tribe's cities than another tribe's. Perhaps we want the unit to always be supported, even if generated nearer to an enemy city than a friendly one. To achieve this, we need to find the nearest friendly city to a unit. This is how we do that:

local function nearestFriendlyCity(unit)

local bestCitySoFar = nil local bestDistanceSoFar = 1000000 local function distance(tileA,tileB) local xDist = tileA.x-tileB.x local yDist = tileA.y-tileB.y return math.sqrt(xDist^2+yDist^2) end for city in civ.iterateCities() do if city.owner == unit.owner and distance(city.location,unit.location) < bestDistanceSoFar then bestCitySoFar = city bestDistanceSoFar = distance(city.location,unit.location) end end return bestCitySoFar

end

GetStartedWithLuaLesson4-06-NearestFriendlyCity.jpg

In this function, we are trying to find the city with the 'best' distance to the unit, with best meaning smallest in this case. The basic idea is that we keep track of the best city that we've found so far, and the distance associated with that city. If we find a 'better' city, we update the information in bestCitySoFar and bestDistanceSoFar. At some point, we will find a 'best' city, and no other city will be better, so that city will be in the bestCitySoFar variable once we reach the end of the list. Note that if there are several cities that have the same distance from the unit, the one that will be returned is the first one checked.

Note that we can define local functions inside our function, to provide us with functionality that might not be needed by any other function. We saw this before, but it was worth pointing out explicitly.

Notice that we've defined this function locally to the Events.lua file, but we might want to try this out from the console, to see if it is working as expected. One way to do that is to make it a global function, which would mean we would have to worry about a function (or any variable) with the same name in any file loaded by our events. Another way is to make a global table, and copy the function as an entry in the table.

console = {}

console.nearestFriendlyCity = nearestFriendlyCity

Try using this function from the console.


Distance in Civilization II

At some point in school, you probably learned that the distance between two points GetStartedWithLuaLesson4-Math1.png and GetStartedWithLuaLesson4-Math2.png is given by the formula

GetStartedWithLuaLesson4-Math3.png.

Generally speaking, when a Civ II game mechanic needs a distance measure of some sort, it uses this distance, or possibly some approximation of it. However, you have probably noticed that when units move, the "distance" they move in one "step" will be different depending on the direction of the step. For example, a unit at square (10,10) can move to square (11,11), which is a distance of about 1.41. However, if the unit instead moves to (12,10), it has moved a distance of 2. This can make thinking about distance in Civ II difficult, especially if you want to think in terms of unit "steps".

It turns out that mathematicians have other notions of "distance" than GetStartedWithLuaLesson4-Math3.png, and one of them happens to be extremely convenient for Civilization II:

GetStartedWithLuaLesson4-Math4.png

This is sometimes called the "Taxicab Distance" or the "Manhattan Distance," since it is the distance between two points when you have to travel on a grid road system. Under this distance system, all unit "steps" have a distance of 2, regardless of direction. Hence, you can find the number of steps a unit must take to get from one tile to another (assuming it can cross all the tiles) by using the Taxicab Distance between the two tiles, and dividing by 2.

"Circles" (by which we mean all the points which are located at a specified distance from a "center") are "diamond" shaped by this distance measure, which is the same shape as all the tiles an air unit can reach in a turn from its own tile.

If you need a distance measure in your scenario, it is highly likely that the Taxicab distance will be as good, if not better better, than the ordinary "Euclidean" distance you are used to. I'm pretty sure that every time I needed a "distance" in Over the Reich, I used the Taxicab distance.

Extra Conditions for Production Selection

A very useful feature of Lua events is the ability to change what will appear in the production menu of a city. We use the following function from the scenario library

civ.scen.onCanBuild(function (defaultBuildFunction, city, item) -> boolean)

The function we register in civ.scen.onCanBuild will return true if we want city to be able to build item, and false otherwise. defaultBuildFunction(city,item) returns true if the default game behaviour would be to let the city build the item, and false otherwise. We use defaultBuildFunction any time we don't want to handle a particular item and/or city, and also any time we want to make sure that all the game's normal conditions are fulfilled for making the item available, even if we have additional conditions to impose.

For a simple example, let us require that caravan units require a marketplace in the city in order to be built.

We begin by introducing an improvementAliases table, adding the marketplace to the table, and adding the caravan unit to the unitAliases table.

improvementAliases={}
improvementAliases.marketplace = civ.getImprovement(5) 

unitAliases.caravan = civ.getUnitType(48) 

Next, we write our function:

local function inBuildMenu(defaultBuildFunction,city,item)
if civ.isUnitType(item) then
   if item == unitAliases.caravan then
       return civ.hasImprovement(city,improvementAliases.marketplace) and defaultBuildFunction(city,item)
   end
end
return defaultBuildFunction(city,item)
end 
civ.scen.onCanBuild(inBuildMenu)


GetStartedWithLuaLesson4-07-CanBuild.jpg

if civ.isUnitType(item) then exists because we will get an error if we try to compare an improvement object with a unit type object.

if item == unitAliases.caravan then
   return civ.hasImprovement(city,improvementAliases.marketplace) and defaultBuildFunction(city,item)
end 

If the item is a caravan, then we need to know if there is a marketplace in the city, and if the city could build a caravan anyway. If both are true, then the city can build a caravan, and the function will, indeed, return true. If either is false, the and will be false, and inBuildMenu will return false as desired.

If the item is not a unit, we skip the corresponding if statement, and return whatever the default function decides for that item. If the item is a unit, but not a caravan, then, again, we reach the line return defaultBuildFunction(city,item) and return whatever the default is.

Try this code out, and see if it works. Note that the Romans don't have trade, and so can't build caravans anyway. Check the Carthaginian cities to see if it works.

Counting Things

Something we might like to do in our events is to count things. For example, suppose we want to limit the number of legions that a tribe can build. This will require us to know how many legions the tribe currently has, and also how many legions the tribe is allowed to have.

 local function countUnits(unitType,tribe)
   local numberSoFar = 0
   for unit in civ.iterateUnits() do
       if unit.owner==tribe and unit.type==unitType then
           numberSoFar = numberSoFar+1
       end
   end
   return numberSoFar
end 

GetStartedWithLuaLesson4-08-CountUnits.jpg

The idea of this function is very similar to nearestFriendlyCity, except that we're incrementing a counter instead of updating our current best answer.

We can do more complicated things, also. Let us make the number of legions a tribe can build dependent on the population of cities that the tribe founded. (I.e. exclude the population of cities the tribe has captured.) We will use the city value city.originalOwner to determine whether the tribe has founded the city or not. Try writing a function findNativePopulation(tribe) to find the population of cities that the tribe has both founded and currently owns. Then add it, along with countUnits to the Events.lua file, and make them both accessible to the console. Finally try them out in the console.


Scroll down for the Answer:







 local function findNativePopulation(tribe)
   local populationSoFar = 0
   for city in civ.iterateCities() do
       if city.owner==tribe and city.originalOwner == tribe then
           populationSoFar = populationSoFar + city.size
       end
   end
   return populationSoFar
end

GetStartedWithLuaLesson4-09-NativePopulation.jpg


Now, let us remove legions from city production, if the tribe's native population is less than 4 times the number of legions the tribe currently owns. That is, there must be 4 "native" citizens per legion. Try implementing this in inBuildMenu.

Scroll Down for Answer:








The relevant part of the code is

 if item == unitAliases.legion then
	 if not defaultBuildFunction(city,item) then
	         return false
	 else
	         return countUnits(unitAliases.legion,city.owner) <= findNativePopulation(city.owner)/4
	 end
 end

GetStartedWithLuaLesson4-10-CanBuildLegion.jpg

if not defaultBuildFunction(city,item) then 
   return false

If the defaultBuildFunction returns false, then we don't have to worry about counting population or legions, so we can return false immediately.

else return countUnits(unitAliases.legion,city.owner) <= findNativePopulation(city.owner)/4 end

If we get to this part of the code, the default build function is true, so we don't have to take it into account anymore. We simply return the result of whether the tribe has fewer legions than it is allowed.

I wrote the code to check if the city satisfies the defaultBuildFunction first, since if it doesn't, there is no point in running the two counting functions. These lessons won't usually worry too much about computational efficiency in code, since computers these days are fast. However, it is worth noting that determining if a legion can be built requires checking every unit and every city in the game. It is possible (though I don't know for sure) that if we were doing this for dozens of units, it might create a lag in bringing up the build choices window. Hence, it is best not to make the check if we already know that the city can't build the unit anyway. I'll discuss efficiency a little bit more in a later lesson.

Changing A Produced Unit

Unfortunatly, at the time of writing, ToTPP doesn't provide the functionality to check or set the production order of a city. This is inconvenient for a number of reasons, one of them being that we can't check the production of other cities when deciding if a city can build a unit or improvement. It is therefore possible, in our legion example, that we run into a situation where 2 cities try to produce a legion in a turn, but only one more legion can be "supported." One option we have is to simply allow the extra legion to be produced. Another option, however, is to do something with the on City Production event trigger civ.scen.onCityProduction(function (city, prod) -> void).

One thing we can do is delete the extra legion, and replace it with a different unit, say an archer. For example,

 local function doOnCityProduction(city,prod)
   if civ.isUnit(prod) then
   	if prod.type==unitAliases.legion and countUnits(unitAliases.legion,city.owner) > findNativePopulation(city.owner)/4 then
   		local substituteUnit = civ.createUnit(unitAliases.archers,city.owner,city.location)
   		substituteUnit.veteran = city:hasImprovement(improvementAliases.barracks)
   		civ.deleteUnit(prod)
   	end
   end
end
civ.scen.onCityProduction(doOnCityProduction)

GetStartedWithLuaLesson4-11-ReplaceLegion.jpg

Try out this code in ClassicRome, by scheduling more legions to finish construction on a turn than can be supported.

The first thing to mention is the line

substituteUnit.veteran = city:hasImprovement(improvementAliases.barracks).

As I discovered while writing this lesson, the game does not bestow veteran status on a newly created unit until after the onCityProduction function is run. This means we can't use the veteran status of the legion to apply it to the Archer. This also means that removing the veteran status bestowed by a barracks will require a bit of work and some functionality we haven't covered yet.

The next thing to note is that I've used the expression findNativePopulation(city.owner)/4, so now there are two places that use '4' for the same purpose. We should create the parameter parameters.populationPerLegion=4 and use that where we need it in place of '4'. That way, if we want to make a change, we don't have to search for every '4' in the file and determine if it corresponds to a legion cost. Do this now.

We make the check civ.isUnit(prod) for the same reason we check if item is a unit type in our inBuildMenu function.

Another alternative we have to replacing the produced unit is to simply return the shields expended for production. Unfortunately, the remaining shields in the production box (after adding the city's production for the turn and removing the cost of the unit) are not eliminated until after the onCityProduction function is executed. Since we have not enabled production carryover in the rules.txt, giving shields to the city is ineffective. We'll revisit this in a future lesson.

Upgrading a Unit Beyond Veteran Status

Specifying A Region

Conclusion