XP and Promos to units added via lua

LeeS

Imperator
Joined
Jul 23, 2013
Messages
7,241
Location
Illinois, USA
When a unit is added to the game directly via lua such units are "born" without the XPs or the promotions that are given via buildings present in the "creating city". The limitation also applies to units created via the <Building_FreeUnits> table or the <Buildings> column <InstantMilitaryIncrease>, so using a dummy building is not actually a solution.

This can of course be "fixed" as part of an lua program, and I have a working chunk of code to do so, but there are a couple of thoughts I have for which I would like some feedback based on others' experience.

  • As part of my Knights Templar World Wonder mod, a special "Crusader" unit is given every X turns after the wonder is completed. In order to track how many XP ought to be given to the unit(s) when they are spawned, I am creating an lua table that stores the ID's and the XP amounts of all buildings that give experience to the unit's domain and/or unitcombat type, as well as any buildings that give XP via the <Buildings> column <Experience>:
    Spoiler XP Buildings Table :
    Code:
    sRequiredCombatClass = tostring(GameInfo.Units{ID=iFreeUnit}().CombatClass)
    sRequiredUnitDomain = tostring(GameInfo.Units{ID=iFreeUnit}().Domain)
    
    print("In looking through buildings to create the XP table")
    for row in GameInfo.Buildings() do
    	iTotalFreeExperience = 0
    	iBuildingID = row.ID
    	sBuildingType = tostring(row.Type)
    	iExperience = row.Experience
    	if iExperience > 0 then
    		iTotalFreeExperience = iTotalFreeExperience + iExperience
    	end
    	for row in GameInfo.Building_DomainFreeExperiences() do
    		if tostring(row.BuildingType) == sBuildingType then
    			if tostring(row.DomainType) == sRequiredUnitDomain then
    				iTotalFreeExperience = iTotalFreeExperience + (row.Experience)
    			end
    		end
    	end
    	for row in GameInfo.Building_UnitCombatFreeExperiences() do
    		if tostring(row.BuildingType) == sBuildingType then
    			if tostring(row.UnitCombatType) == sRequiredCombatClass then
    				iTotalFreeExperience = iTotalFreeExperience + (row.Experience)
    			end
    		end
    	end
    	---------------------------------------------------------------------------------------------------
    	--store the building ID and the total XP given by it
    	--do not make additions to the table gValidBuildingExperience anywhere but in the following lines
    	---------------------------------------------------------------------------------------------------
    	if iTotalFreeExperience > 0 then
    		gValidBuildingExperience[iBuildingID] = iTotalFreeExperience
    	end
    end
  • In order to "trap" a list of all buildings that could give <TrainedFreePromotion> to the unit, I construct a second table to store the building ID's and promotion ID's of TrainedFreePromotions that would apply to my unit:
    Spoiler Promo Buildings Table :
    Code:
    for row in GameInfo.Buildings() do
    	iBuildingID = row.ID
    	sPromoString = tostring(row.TrainedFreePromotion)
    	--print("sPromoString is set to " .. sPromoString .. " with a building ID number of " .. tostring(iBuildingID))
    	if sPromoString ~= "nil" then
    		bWasAPromoMatch = false
    		bWasACombatClassMatch = false
    		for row in GameInfo.UnitPromotions_UnitCombats() do
    			if tostring(row.PromotionType) == sPromoString then
    				bWasAPromoMatch = true
    				if tostring(row.UnitCombatType) == sRequiredCombatClass then
    					bWasACombatClassMatch = true
    				end
    			end
    		end
    		if bWasAPromoMatch then
    			if bWasACombatClassMatch then
    				gValidBuildingPromos[iBuildingID] = GameInfoTypes[sPromoString]
    			end
    		else gValidBuildingPromos[iBuildingID] = GameInfoTypes[sPromoString]
    		end
    	end
    end
  • These tables are created when the mod loads.

Later, when the unit is spawned, I access the two tables and check for buildings that are present in the city which constructed the wonder, and give correct XPs and promotions accordingly:
Spoiler Generate XP and Promos :
Code:
local iXPS = 0
for k,v in pairs(gValidBuildingExperience) do
	if pCity:IsHasBuilding(k) then
		iXPS = iXPS + v
	end
end

iUnitPlot = pCity:Plot()
pUnitToSpawn = pPlayer:InitUnit(iFreeUnit, iUnitPlot:GetX(), iUnitPlot:GetY())
pUnitToSpawn:JumpToNearestValidPlot()
pUnitToSpawn:SetExperience(iXPS)
for k,v in pairs(gValidBuildingPromos) do
	if pCity:IsHasBuilding(k) then
		pUnitToSpawn:SetHasPromotion(v, true)
	end
end
So:
  1. If a promotion is given using pUnitToSpawn:SetHasPromotion(v, true) will this cause a crash or other problem if the unit in question has already been given the promotion? IE, if more than one building in the database gives, say, the Amphibious promotion, and both buildings are present in the city?
  2. Is there a more direct way of pulling as a string the unit's domain and combat class than what I am using?
    Code:
    sRequiredCombatClass = tostring(GameInfo.Units{ID=[COLOR="Blue"]iFreeUnit[/COLOR]}().CombatClass)
    sRequiredUnitDomain = tostring(GameInfo.Units{ID=[COLOR="blue"]iFreeUnit[/COLOR]}().Domain)
    I am thinking of adapting this method to another mod I have where a unit that is given via lua changes as a player progresses through the tech tree, so the ID # of the unit as expressed within iFreeUnit will change during the game. Obviously I will have to "re-create" the tables with the correct XP / Promotions / Buildings combinations when this occurs, but I am looking to be sure I am not taking the "pointlessly long way round" to get the info needed regarding the unit's Domain and Combat Class.
 
1) pUnitToSpawn:SetHasPromotion(v, true); pUnitToSpawn:SetHasPromotion(v, true); has the same effect as pUnitToSpawn:SetHasPromotion(v, true); and won't crash

2) you don't need the tostring() wrapping the columns - as they are strings. Also, while one more line of code, this will execute quicker as it only access the database once
Code:
local freeUnitDetails = GameInfo.Units{ID=iFreeUnit}()
sRequiredCombatClass = freeUnitDetails.CombatClass
sRequiredUnitDomain = freeUnitDetails.Domain
 
1) pUnitToSpawn:SetHasPromotion(v, true); pUnitToSpawn:SetHasPromotion(v, true); has the same effect as pUnitToSpawn:SetHasPromotion(v, true); and won't crash
That confirms what I had thought would happen, before I started questioning myself and worrying it might start causing players' games to crash five minutes after I released an update of the mod.
2) you don't need the tostring() wrapping the columns - as they are strings. Also, while one more line of code, this will execute quicker as it only access the database once
Code:
local freeUnitDetails = GameInfo.Units{ID=iFreeUnit}()
sRequiredCombatClass = freeUnitDetails.CombatClass
sRequiredUnitDomain = freeUnitDetails.Domain
*Giant Lightbulb* just went off over my head with the lua table symbols {}. As well as the *duh*, those are already strings, no need to string them. For some reason I was thinking I might get the Unit_CombatClass and Domains tables ID#s rather than the actual string within the column. :D
 
For some reason I was thinking I might get the Unit_CombatClass and Domains tables ID#s rather than the actual string within the column. :D

In which case tostring() (a core Lua function, nothing to do with Civ and Firaxis) would have just given you the character representation of the numeric id and not the Type column from the associated table
 
Coming into this a little late, but here is a bit of code I developed to add experience and promotions to units spawned in a city via lua.

The discussion above is a bit above my level of ability, but the code below does work. I forget exactly, but it may also require Machiavelli's unit produced code, fon't remember... probably, the code should be changed to reflect the fact that a unit produced event has been added in the recent patch.

Of course, there would also need to be alterations to reflect the civilization and units.

I build the code to force build units by the AI... that part of the code can be done away with, of course.

Code:
-- To spawn a unit in capital of France and deduct purchase price while adding promotion and experience from city.

function FranceUnitPurchase (iPlayer)

	local IsFrance = false;
	local pPlayer = Players[iPlayer] 
	
	--determine if Player is France and is not human 

	if (GameInfo.Civilizations.CIVILIZATION_FRANCE.ID == pPlayer:GetCivilizationType()) and pPlayer:IsAlive () and pPlayer:GetNumCities() >= 1 and not pPlayer:IsHuman() then
					
		IsFrance = true;

	end	

	if (IsFrance == true ) then
	
		local pFrance = pPlayer
		local pUnit = GameInfoTypes["UNIT_HORSEMAN"]
		local pCity = pFrance:GetCapitalCity()
		local cost = pCity:GetUnitPurchaseCost( pUnit );
		local gold =  pFrance:GetGold()
		local pPlot = pCity:Plot()
		local iPromotion
		local iActiveUnit
		local ActiveUnit
		
		-- determine if France can purchase unit in capital and has enough gold then spawn and deduct gold

		if (pCity:IsCanPurchase(true, true, pUnit, -1, -1, YieldTypes.YIELD_GOLD)) and gold>=cost then
			
			local SpawnUnit;
			local iSpawnX = pPlot:GetX();
			local iSpawnY = pPlot:GetY();
			
			-- spawn the unit and deduct gold

			SpawnUnit = pFrance:InitUnit(GameInfoTypes["UNIT_HORSEMAN"], iSpawnX, iSpawnY, UNITAI_ATTACK, DIRECTION_NORTHWEST )
			SpawnUnit:SetExperience(pCity:GetDomainFreeExperience( SpawnUnit:GetDomainType())) 
			SpawnUnit:SetMoves(0)
			pFrance:ChangeGold( -cost )
		
			-- loop for promotions from buildings in city	
		
			for promotion in GameInfo.UnitPromotions() do

			iPromotion = promotion.ID	

				-- check for promotions from city

				if ( pCity:GetFreePromotionCount( iPromotion ) > 0 ) then

				-- find units in city

					local ActiveUnit = pPlot:GetUnit(i)
					local iActiveUnit = GameInfo.Units[ActiveUnit:GetUnitType()].ID
			
					-- see if unit created this turn

					if ActiveUnit:GetGameTurnCreated() == Game.GetGameTurn() then

					--check if unit can get promotion and give it

						-- looks for unit combat class and changes it to string

						local sCombatClass = "none"
						for row in GameInfo.Units() do
							if row.ID == iActiveUnit then
							sCombatClass = tostring(row.CombatClass)
							--print("...finding CombatClass of...", ActiveUnit:GetName())
							end
						end

						--looks for promotion combat type (class) and changes it to string

						local sType = "none"
						for row in GameInfo.UnitPromotions() do
							if row.ID == iPromotion then
								sType = tostring(row.Type)
						--		print("...finding Promotion Type...", sType)
							end
						end	

						local sUnitCombatType = "none"
						local sPromotionType = "none"
						for row in GameInfo.UnitPromotions_UnitCombats() do
							
							sPromotionType = tostring(row.PromotionType)
							sUnitCombatType = tostring(row.UnitCombatType)
							--print("...finding... Promotion Combat Type...", tostring(row.UnitCombatType))
							if sType == sPromotionType	then
								--print("Checking if...", sType, "... equals...", sPromotionType)

								if sUnitCombatType == sCombatClass then

									print(pFrance:GetName(), "...spawned unit......getting Promotion...", ActiveUnit:GetName() )
									ActiveUnit:SetHasPromotion(iPromotion, true);

								end
							end
						end
					end
				end
			end
		end	
	return

	else
	return
	end

end

GameEvents.PlayerDoTurn.Add(FranceUnitPurchase)
 
I was originally using the pCity:GetDomainFreeExperience( SpawnUnit:GetDomainType())) method, but it didn't seem to be 'capturing' all the experience that could be given in a city to a unit of Domain_X. I'm pretty sure it misses anything given by a building via the Experience column, and I'm not sure it captures anything given through the Building_UnitCombatFreeExperiences table. Which was the original reason I re-wrote the code as I did. And I figured since I was using the method I adopted I could also pretty easily adapt for the unit promotions as well.

I'm assuming I get a slight processing-speed improvement in my Knights Templar mod from doing all these "look-ups" at game loading rather than every time the unit is spawned, but I am likely also paying a processing-speed penalty by looking through the two tables I created and checking whether or not the city has the buildings defined within the two tables. But I am also not running through the code to spawn the units very often. In Quick game-speed the spawning code will only run once every seven turns, for example.

The other mod I am adapting this method to will also run only once every 15 turns on Standard Game speed, and then only once on that turn. So the issue (for my two mods) with processing speed is probably moot, really.

For me the real issue was the missing experience amounts that I seemed to be seeing.

Though the method I am using doesn't capture the experiences given from great works whereas I'm pretty sure the city:GetDomainFreeExperience() does capture that.

I'm still trying to figure out how to determine whether a particular building's great works slots are filled or not. I haven't found anything in the wiki, so I'm guessing I need to find the old thread about the added hooks and methods that were made available in G&K and BNW before last fall's additions. But I would also need to figure how to deal with compatibility between expansions within the lua so that it doesn't barf when it sees a reference to the Building_DomainFreeExperiencePerGreatWork table and the user has either Vanilla or G&K.
 
Back
Top Bottom