Get Started With Lua Events Lesson 5: Even More Examples
Back to Get Started With Lua Events
This lesson is currently under construction.
You can find the copy of events that I started Lesson 5 with here.
Fixing The Sea Unit Disbandment Bug
In Civilization II, when a city is captured, all the units supported by that city are disbanded. It turns out that the civ.scen.onCityTaken(function (city, defender) -> void)
event trigger happens before units are disbanded. This means that we can use the civ.scen.onCityTaken
event trigger to do things involving soon to be disbanded units.
One thing we can do is perform the disbanding action ourselves. The reason for this is that a bug exists that disbands all the units on a sea square if a ship there is disbanded (try it). This is obviously very undesirable, but we can override it in the case of city capture by doing the disbandment ourselves.
Essentially, when a city is captured we do a for loop over all the units in the game, and if the unit is supported by the captured city, or carried by a unit supported by it, then we delete the unit. Units disbanded due to city capture that are in other cities do not contribute shields to production, so we don't have to worry about that.
The first attempt at the code was:
local function doOnCityTaken(city,defender)
for unit in civ.iterateUnits() do
if unit.homeCity == city or (unit.carriedBy and unit.carriedBy.homeCity == city) then
civ.deleteUnit(unit)
end
end
end
This code goes through all the units in the game and checks if the unit in question has its home as the captured city or if it is being carried by a unit homed to the captured city. If either of those conditions is met, the unit is deleted.
I was expecting this code not to be correct because in some instances, the ship would be deleted before the unit being carried. However, when I introduced some print statements into the code, I found that a Celtic settler near Milan was being deleted (when I was capturing a Carthaginian city), since the game decided it was being "carried" by a deleted caravan that had the home city of the city I was capturing.
Running an appropriate print statement for all the units in the game revealed that many units are being "carriedBy" something that doesn't make any sense. I don't know if this is an artifact of the fact that this scenario was converted from an older version of the game, but it is best to account for it anyway.
Thus, my second iteration of the code is
local function doOnCityTaken(city,defender)
for unit in civ.iterateUnits() do
if unit.homeCity == city then
for sameSquareUnit in unit.location.units do
if sameSquareUnit.carriedBy == unit and sameSquareUnit ~= unit then
print(sameSquareUnit,"deleted.")
civ.deleteUnit(sameSquareUnit)
end
end
print(unit,"deleted")
civ.deleteUnit(unit)
end
end
end
civ.scen.onCityTaken(doOnCityTaken)
This code will not delete aircraft on a carrier (and will also not delete just created units that haven't been moved with the ship, presumably since they haven't been given a carriedBy entry yet), which probably isn't a big concern for our purposes, though it might be in other cases. However, this code will delete a unit in port that was carried there by a disbanded unit (though the ground unit itself is not supported by the captured city). It appears that carriedBy is not cleared upon entering port, so we should check that our unit is actually at sea before checking the square for units being carried by it. Since carriedBy isn't reliable, let's also check that the "carrying" unit is a sea unit and also has a carrying capacity before checking the square.
local function doOnCityTaken(city,defender)
for unit in civ.iterateUnits() do
if unit.homeCity == city then
if unit.location.terrainType % 16 == terrainAliases.ocean and unit.type.domain == 2 and unit.type.hold > 0 then
for sameSquareUnit in unit.location.units do
if sameSquareUnit.carriedBy == unit and sameSquareUnit ~= unit then
print(sameSquareUnit,"deleted.")
civ.deleteUnit(sameSquareUnit)
end
end
end
print(unit,"deleted")
civ.deleteUnit(unit)
end
end
end
civ.scen.onCityTaken(doOnCityTaken)
This function appears to work (with the exception of air units surviving a carrier disbandment).
Keeping Units Supported by a Captured City
Instead of disbanding a unit when its home city is captured, we could give it a new home city instead. It would be fairly natural to transfer support to either the nearest friendly city to the city captured, or to the nearest friendly city to the now unsupported unit. However, there is a complicating problem: cities can only support a maximum number of units, and it wouldn't do to transfer unit support only to have units disbanded anyway since there weren't enough shields to support them.
What we'd like to do is to determine if a city can support more units, and if not, exclude it from the list of cities that we're considering transferring the unit to. Unfortunately, there isn't an easy way to do this. While the command city.totalShield
excludes waste from the result it returns, it does include shields paid for supporting units. Hence, we can't simply check if the result of that function call is 0. Also, since there isn't a command to find how many units the city is supporting, we don't have an easy way to determine if the city can support extra units.
Since there isn't an automatic way to determine how many units a city is supporting, we'll have to find the information ourselves. For a given city, we can simply go through all the units in the game and figure out which of those units is supported by the city in question, and make a count. However, this will require going through the entire list of units every time we need to find out this information, which could end up being quite a few times.
Instead, we will populate a table with the number of units supported by a city, so we only have to go through the unit list one time for this purpose.
We'll use the city id number as the key for our table. For each unit in the game, if the unit requires support, we'll increment the value corresponding to the id number of its home city. Units no longer on the map do not require support, so we'll have to check for that (deleted units can still be in the unit list, they just won't have valid map coordinates). We'll also have to check if the unit has the role of diplomat or caravan, since they, then, won't require support. We won't check for free support under fundamentalism, since such units still use up one of the government's support slots. (Try putting a bunch of fanatics in a city, then add another unit. That other unit will cost support.)
The code to do this is as follows:
local function doOnCityTaken(city,defender)
local citySupportTable = {}
for unit in civ.iterateUnits() do
if unit.homeCity and civ.getTile(unit.location.x,unit.location.y,unit.location.z) and unit.type.role <= 5 then
citySupportTable[unit.homeCity.id] = citySupportTable[unit.homeCity.id] or 0
citySupportTable[unit.homeCity.id] = citySupportTable[unit.homeCity.id]+1
end
end
local function canSupportAnotherUnit(city)
local freeSupport = 0
local govtNumber = city.owner.government
if govtNumber <= 1 then
-- anarchy or despotism
freeSupport = city.size
elseif govtNumber == 2 then
-- monarchy
freeSupport = civ.cosmic.supportMonarchy
elseif govtNumber == 3 then
-- communism
freeSupport = civ.cosmic.supportCommunism
elseif govtNumber == 4 then
freeSupport = civ.cosmic.supportFundamentalism
end
-- make sure citySupportTable has an entry for this city
citySupportTable[city.id] = citySupportTable[city.id] or 0
if freeSupport+city.totalShield - citySupportTable[city.id] > 0 then
return true
else
return false
end
end
local function taxiDistance(tileA,tileB)
return math.abs(tileA.x-tileB.x)+math.abs(tileA.y-tileB.y)
end
for unit in civ.iterateUnits() do
if unit.owner == defender and unit.homeCity == city and civ.getTile(unit.location.x,unit.location.y,unit.location.z) then
local bestCitySoFar = nil
local bestDistanceSoFar = 1000000
for candidateCity in civ.iterateCities() do
if candidateCity.owner == defender and canSupportAnotherUnit(candidateCity)
and taxiDistance(candidateCity.location,unit.location) <bestDistanceSoFar then
bestCitySoFar = candidateCity
bestDistanceSoFar = taxiDistance(bestCitySoFar.location,unit.location)
end
end
unit.homeCity = bestCitySoFar
if unit.type.role <= 5 then
citySupportTable[bestCitySoFar.id]= (citySupportTable[bestCitySoFar.id] or 0)+1
end
end
end
end
Let us take a closer look at this code
local citySupportTable = {}
for unit in civ.iterateUnits() do
if unit.homeCity and civ.getTile(unit.location.x,unit.location.y,unit.location.z) and unit.type.role <= 5 then
citySupportTable[unit.homeCity.id] = citySupportTable[unit.homeCity.id] or 0
citySupportTable[unit.homeCity.id] = citySupportTable[unit.homeCity.id]+1
end
end
This part of the code populates a table that counts how many units each city in the game is supporting.
if unit.homeCity and civ.getTile(unit.location.x,unit.location.y,unit.location.z) and unit.type.role <= 5 then
This checks if the unit actually has a home city, and if the unit is actually on the map (destroyed units remain for a while, but have a location that does not correspond to a map tile). Also, units with role 6 or 7 do not require support, and so do not contribute to the count.
citySupportTable[unit.homeCity.id] = citySupportTable[unit.homeCity.id] or 0
citySupportTable[unit.homeCity.id] = citySupportTable[unit.homeCity.id]+1
The first line makes sure that there is an entry in citySupportTable corresponding to the unit's home city. If the city hasn't been put there yet, citySupportTable[unit.homeCity.id] will be nil and nil or 0 evaluates to 0, so an entry is placed in the table. Since we now know that the entry exists and is an integer, we add 1 to the count for the new unit.
local function canSupportAnotherUnit(city)
local freeSupport = 0
local govtNumber = city.owner.government
if govtNumber <= 1 then
-- anarchy or despotism
freeSupport = city.size
elseif govtNumber == 2 then
-- monarchy
freeSupport = civ.cosmic.supportMonarchy
elseif govtNumber == 3 then
-- communism
freeSupport = civ.cosmic.supportCommunism
elseif govtNumber == 4 then
freeSupport = civ.cosmic.supportFundamentalism
end
-- make sure citySupportTable has an entry for this city
citySupportTable[city.id] = citySupportTable[city.id] or 0
if freeSupport+city.totalShield - citySupportTable[city.id] > 0 then
return true
else
return false
end
end
Since this is a local function defined within doOnCityTaken, it will only be available within doOnCityTaken. This function will check if a city can support another unit. To do this, we add up the free unit support the city gets due to its government and the number of shields the city is reported as producing. If this is more than citySupportTable says the city is supporting, then the city is able to support additional units, so we can return true. Otherwise, the function returns false.
local function taxiDistance(tileA,tileB)
return math.abs(tileA.x-tileB.x)+math.abs(tileA.y-tileB.y)
end
for unit in civ.iterateUnits() do
if unit.owner == defender and unit.homeCity == city and civ.getTile(unit.location.x,unit.location.y,unit.location.z) then
local bestCitySoFar = nil
local bestDistanceSoFar = 1000000
for candidateCity in civ.iterateCities() do
if candidateCity.owner == defender and canSupportAnotherUnit(candidateCity)
and taxiDistance(candidateCity.location,unit.location) <bestDistanceSoFar then
bestCitySoFar = candidateCity
bestDistanceSoFar = taxiDistance(bestCitySoFar.location,unit.location)
end
end
unit.homeCity = bestCitySoFar
if unit.type.role <= 5 then
citySupportTable[bestCitySoFar.id]= (citySupportTable[bestCitySoFar.id] or 0)+1
end
end
end
This code checks every unit to see if it is on the map and supported by the city that has just been captured. If so, code to find the nearest eligible city to move support to is executed. This code is similar to other code to find the nearest city that we've seen, the difference being that we also check if the city can support units in addition to whether the city is owned by the correct civ.
unit.homeCity = bestCitySoFar
if unit.type.role <= 5 then
citySupportTable[bestCitySoFar.id]= (citySupportTable[bestCitySoFar.id] or 0)+1
end
This transfers the home city of the unit, and, if necessary, updates the citySupportTable to reflect that a new unit is supported.
Comment out the old doOnCityTaken function and write this one into the events instead. Test it out.