Ranged units capture city if adjacent

Pazyryk

Deity
Joined
Jun 13, 2008
Messages
3,584
My mod has the whole archer line-up acting like G&K machine gunners (range 1). I want these units to be able to capture cities if they knock them down to 0 hp.

I've already devised a way to do this, but I was hoping someone would tell me a better way (without need for plot iteration):
  1. Use Events.EndCombatSim to identify any archer that has made an attack and is alive (attacking unit alive, took no damage, and has special PROMOTION_ARCHER_ CONQUEROR).
  2. Iterate through all adjacent plots and look for city with 0 hp.
  3. If there is one: convert city, kill remaining units, move archer to plot.

No AI needed here because AI will move these adjacent to cities due to having only range 1 (I don't allow any range extending promos for these). I just wish the AI was capable of move & shoot in one turn.
 
SpecificCityInfoDirty(PlayerID player, CityID cityID, CityUpdateType updateType) in Events, and then city:GetDamage(), city:GetMaxHitPoints() should tell you exactly when a city hits 0 or less hitpoints. Check for CityUpdateType = GameInfoTypes.CITY_UPDATE_TYPE_BANNER

Combine that with UI.GetHeadSelectedUnit() and you can assume if the selected unit is an archer it just captured the city :) Account for edge cases, like the city being razed, and any other I'm not thinking of.

You can also get the city's plot: City:Plot() and use Map.PlotDistance(plotX, plotY, unitX, unitY) to check if the archer is distance 1.
 
In addition, you can toggle on and off the SpecificCityInfoDirty event inside Events.WarStateChanged if you want to get fancy. If you are not at war turn it off, otherwise on. Or more likely, to work with the AI, turn it off if nobody is at war and on if anybody is at war.

If GetHeadSelectedUnit doesn't work for the AI (I don't know) then you will have to walk plots around the city but that's easy:

Code:
for i = 0, pCity:GetNumCityPlots() - 1, 1 do
	local plot = pCity:GetCityIndexPlot(i);
end
 
Combine that with UI.GetHeadSelectedUnit() and you can assume if the selected unit is an archer it just captured the city :) Account for edge cases, like the city being razed, and any other I'm not thinking of.

AFAIK UI.GetHeadSelectedUnit() won't catch AI units...

@Pazyryk:

if you can use a DLL, it would be easier, for example whoward's DLL include the CombatEnded event from R.E.D.

Code:
function CombatEnded(iAttackingPlayer, iAttackingUnit, attackerDamage, attackerFinalDamage, attackerMaxHP, iDefendingPlayer, iDefendingUnit, defenderDamage, defenderFinalDamage, defenderMaxHP, iInterceptingPlayer, iInterceptingUnit, interceptorDamage, plotX, plotY)

      -- your code here

end
GameEvents.CombatEnded.Add( CombatEnded)
plotX, plotY give you the attacked plot.
 
Holding off on DLL for a while yet. Not forever, just until things settle down after BNW.

@Hambil, are you sure Events.SpecificCityInfoDirty fires for AI player?

Assuming it does, then it looks like I can detect both a living archer that has attacked (using EndCombatSim) and a city that has been knocked down to 0 hp (using SpecificCityInfoDirty). So all I need to do is design a system to identify these two happening on adjacent plots at about the same time. I'll have to factor in the fact that these graphic Events might be firing after several additional attacks have happened (perhaps an adjacent melee has already taken the city before ether of these Events fires).
 
Holding off on DLL for a while yet. Not forever, just until things settle down after BNW.

@Hambil, are you sure Events.SpecificCityInfoDirty fires for AI player?

Assuming it does, then it looks like I can detect both a living archer that has attacked (using EndCombatSim) and a city that has been knocked down to 0 hp (using SpecificCityInfoDirty). So all I need to do is design a system to identify these two happening on adjacent plots at about the same time. I'll have to factor in the fact that these graphic Events might be firing after several additional attacks have happened (perhaps an adjacent melee has already taken the city before ether of these Events fires).

There could be a problem during the AI turn, the major difficulties I had to restore my WWII mod after patch .674 was linked to the fact that if multiple cities were captured by the same AI, the events related to all the cities were firing before the EndCombatSim events for all the units...

(or the opposite, can't remember :think:)

But as you have very specific rules, yes, you may be able to use it.
 
It makes sense when you realize that these Events are there only for graphics, not intended for hooking game rules. We do it anyway. Just like hobos riding rail cars and complaining when it doesn't' work out so well...
 
AFAIK UI.GetHeadSelectedUnit() won't catch AI units...

Correct. All the UI.Get methods only select human player units/cities
 
Zulu unit in Brave New World supposedly fires and melees... maybe just wait for BNW to provide a simple xml entry.

Would be nice if there was a GameEvents associated with that, but who knows if that't the case. In any case, I can identify the conditions well enough with existing Events.


A more difficult problem (perhaps) is how to conquer the city via Lua. There is no city:Conquer(iNewOwner) method. Will plot:SetOwner() change city owner properly? Even then, I'd have to write a lot of Lua to replicate conquest effects (pop change, building destruction, resistance turns), which would be possible but a real a pain.

Or would the city be conquered if I gave the archer a move and moved him onto the city plot? Or if that doesn't work, convert him to a temp melee unit and order to attack?
 
Use:

AcquireCity(pCity, false, true)

I use it to transfer cities puppet states conquer to allied player civs.

Following code: (you'll want to ignore a lot of it as much is scenario specific.)

You likely know a lot more lua than I, but for what's it's worth...

Code:
-- when a city is captured by a city state, and that city was not originally owned by the city state
-- It will be transferred to a major power ally if such exists (excepting the Franks).

function ChangeOwner (oldPlayerID, bCapital, iX, iY, newPlayerID, bConquest)

local newPlayer = Players[newPlayerID];
local oldPlayer = Players[oldPlayerID]; --not needed
local plot = Map.GetPlot(iX, iY);
local pCity = plot:GetPlotCity();
local Franks
local iFranks
local Essex
local iEssex
local Sussex
local iSussex
local Kent
local iKent
local EastAnglia
local iEastAnglia

-- find Franks

	for iPlayer=GameDefines.MAX_MAJOR_CIVS, GameDefines.MAX_CIV_PLAYERS-1, 1 do  

	local pFranks = Players[iPlayer]

		if (GameInfo.MinorCivilizations.MINOR_CIV_FRANKS.ID == pFranks:GetMinorCivType()) then
	
		Franks = pFranks
		iFranks = iPlayer
		
		end	
	end

-- find Sussex

	for iPlayer=GameDefines.MAX_MAJOR_CIVS, GameDefines.MAX_CIV_PLAYERS-1, 1 do  

	local pSussex = Players[iPlayer]

		if (GameInfo.MinorCivilizations.MINOR_CIV_SUSSEX.ID == pSussex:GetMinorCivType()) then
	
		Sussex = pSussex
		iSussex = iPlayer
		
		end	
	end

-- find Essex

	for iPlayer=GameDefines.MAX_MAJOR_CIVS, GameDefines.MAX_CIV_PLAYERS-1, 1 do  

	local pEssex = Players[iPlayer]

		if (GameInfo.MinorCivilizations.MINOR_CIV_ESSEX.ID == pEssex:GetMinorCivType()) then
	
		Essex = pEssex
		iEssex = iPlayer
		
		end	
	end

-- find Kent

	for iPlayer=GameDefines.MAX_MAJOR_CIVS, GameDefines.MAX_CIV_PLAYERS-1, 1 do  

	local pKent = Players[iPlayer]

		if (GameInfo.MinorCivilizations.MINOR_CIV_KENT.ID == pKent:GetMinorCivType()) then
	
		Kent = pKent
		iKent = iPlayer
		
		end	
	end

-- find EastAnglia

	for iPlayer=GameDefines.MAX_MAJOR_CIVS, GameDefines.MAX_CIV_PLAYERS-1, 1 do  

	local pEastAnglia = Players[iPlayer]

		if (GameInfo.MinorCivilizations.MINOR_CIV_EAST_ANGLIA.ID == pEastAnglia:GetMinorCivType()) then
	
		EastAnglia = pEastAnglia
		iEastAnglia = iPlayer
		
		end	
	end

-- find major civilization

	for iPlayer=0, GameDefines.MAX_MAJOR_CIVS-1 do 

	local pAlly = Players[iPlayer]

		if pAlly:IsAlive() and newPlayer:IsAllies(iPlayer) then
	
		Ally = pAlly
				
		end	
	end

-- City exists, not Frank ,is not originally owned by Minor and Minor is allied with Ally?

    if newPlayer:IsMinorCiv() and pCity ~= nil and (pCity:GetOwner() ~= iFranks) and (pCity:GetOwner() ~= iSussex) and (pCity:GetOwner() ~= iEssex) and (pCity:GetOwner() ~= iKent) and (pCity:GetOwner() ~= iEastAnglia) and (pCity:GetOriginalOwner() ~= newPlayerID) then
			
			print("Transferring...", pCity:GetName(), "to" , Ally:GetName(), "from", newPlayer:GetName() )
						
			Ally:AcquireCity(pCity, false, true)
			pCity:DoTask(TaskTypes.TASK_UNRAZE);
	end

end

GameEvents.CityCaptureComplete.Add(ChangeOwner)

You'll have to tie it to some other event though.
 
When a city is capture in the DLL, it is destroyed and recreated then all the stuff they wanted added back in (buildings, etc.) is added back. For a peak at their logic (I happen to be going over this code myself for my tweak that keeps free buildings granted by wonders when a city is captured) check out: CvPlayer::acquireCity but the logic is lengthy and nasty. ha - cross posted :)
 
Ah! Thanks. I forgot about player:AcquireCity. And the 2nd arg appears to be a conquest flag. I wonder if that will trigger downstream conquered city effects for me? (Or perhaps it is just informational so the game knows the city was conquered.)
 
Use:

AcquireCity(pCity, false, true)

for conquest I think it should be player:AcquireCity(pCity, true, false)

first boolean is for conquest, second is for trade.

but if you want to "peacefully" transfer a city, use:

Code:
	pPlayer:AcquireCity(pCity, false, true)
	pCity:SetPuppet(false)
	pCity:ChangeResistanceTurns(-pCity:GetResistanceTurns())
	pCity:SetOccupied(false)
 
Here's the code. I've only just barely tested, but it seems to work. Any ranged unit will conquer a city if they are adjacent and knock it down to 1 hp. This uses my own PlotToRadiusIterator to test adjacent plots (you could substitute whoward69's plot iterator).


Code:
local BARB_PLAYER_ID = GameDefines.MAX_PLAYERS - 1
local MAX_CITY_HIT_POINTS = GameDefines.MAX_CITY_HIT_POINTS

function OnEndCombat(iAttackingPlayer, iAttackingUnit, iAttackingUnitDamage, iAttackingUnitFinalDamage, iAttackingUnitMaxHitPoints, iDefendingPlayer, iDefendingUnit, iDefendingUnitDamage, iDefendingUnitFinalDamage, iDefendingUnitMaxHitPoints, unknown)
	print("Running OnEndCombat ", iAttackingPlayer, iAttackingUnit, iAttackingUnitDamage, iAttackingUnitFinalDamage, iAttackingUnitMaxHitPoints, iDefendingPlayer, iDefendingUnit, iDefendingUnitDamage, iDefendingUnitFinalDamage, iDefendingUnitMaxHitPoints, unknown)
	local attackingPlayer = Players[iAttackingPlayer]
	local attackingUnit = attackingPlayer:GetUnitByID(iAttackingUnit)
	local defendingPlayer = Players[iDefendingPlayer]
	local defendingUnit = defendingPlayer:GetUnitByID(iDefendingUnit)
	--iAttackingUnit and/or iDefendingUnit may be -1, which will result in unit objects that are nil 
	--iDefendingUnitFinalDamage and iDefendingUnitMaxHitPoints still give values even if iDefendingUnit = -1 (gives city stats if city defender)

	--archer city conquest test
	if iAttackingUnitDamage == 0 and not defendingUnit and iAttackingPlayer < BARB_PLAYER_ID and attackingUnit then
		print("Possible archer attack on city; testing for conquest conditions...")
		if iDefendingUnitFinalDamage >= iDefendingUnitMaxHitPoints - 1 then
			for x, y in PlotToRadiusIterator(attackingUnit:GetX(), attackingUnit:GetY(), 1, nil, nil, true) do
				local plot = GetPlotFromXY(x, y)
				local city = plot:GetPlotCity()				--nil if no city here
				if city and city:GetDamage() >= MAX_CITY_HIT_POINTS - 1 then
					if Teams[attackingPlayer:GetTeam()]:IsAtWar(Players[city:GetOwner()]:GetTeam()) then
						print("Detected archer attack with adjacent 0 hp enemy city; teleporting archer to city plot")
						attackingUnit:SetXY(x, y)			--conquers city!
						break
					end
				end
			end
		end
	end
end
Events.EndCombatSim.Add(OnEndCombat)

Conquering the city turned out to be easier than I thought. Teleporting a unit onto an enemy city seems to do the trick. I discovered that by accident from a bug in my mod.
 
Back
Top Bottom