Get Started With Lua Events Lesson 5: Even More Examples

From Scenario League Wiki
Revision as of 20:20, 26 January 2019 by Prof. Garfield (talk | contribs) (Added to category lua resources)
Jump to navigationJump to search

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)

GetStartedWithLuaLesson5-01-DisbandingUnits.jpg

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)

GetStartedWithLuaLesson5-02-DisbandingUnits-2.jpg

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


GetStartedWithLuaLesson5-03-RehomeUnits.jpg

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.

Generating Money With Units On Key Press

Creating and Deleting Ammunition

Creating Land Units At Sea

Unit Attack Bonus with Complementary Units In Square

Double Unit Promotion Revisited

Conclusion