[LUA] Damage per turn in enemy lands? -General Winter Idea

Joined
Apr 12, 2011
Messages
64
Hey everyone,
I am working on a mod that changes the ability of the Kremlin to a "General Winter" of sorts where enemy units lose 10 health per turn while in the owner of the Kremlin's land. Requires to be built in or next to Tundra to give a little bonus to those stuck up in the icy wastes.

I'm pretty decent at XML, but am still sort of fumbling around with Lua, without much luck.

What I have currently is:
Spoiler :
Code:
--Figure out who has the Kremlin
function GetPlayerWithKremlin() 	
	local playerWithKremlin = nil;
	local buildingClassId = GameInfo.BuildingClasses["BUILDINGCLASS_KREMLIN"].ID;	
	
	for playerNum = 0, GameDefines.MAX_CIV_PLAYERS - 1 do
		local player = Players[playerNum];
		local buildingCount = player:GetBuildingClassCount(buildingClassId);
		if buildingCount > 0 then
			playerWithKremlin = player;
			break;
		end
	end	
		
	return playerWithKremlin
end

--General Winter Strikes
function KremlinGeneralWinter(iPlayer, iUnitID, iX, iY)
	if iPlayer ~= nil then
		local playerWithKremlin = GetPlayerWithKremlin();				
		
		--If the Kremlin has not yet been built
		if playerWithKremlin == nil then
			return;
		end

		local unit = Players[ iPlayer ]:GetUnitByID( iUnitID );
		
		--To check if the player is at war with Kremlin Owner
		if unit ~= nil and playerWithKremlin.isAtWar(unit.getOwner()) then
			local plot = unit:GetPlot();			
						
			if plot:GetOwner() == playerWithKremlin:GetID() then
				--Time for General Winter Damage				
				local GeneralWinterDamage = unit:GetCurrHitPoints() -10
				local damage = unit:GetDamage();					
				if GeneralWinterDamage > 0 then
						Unit:SetDamage(10)
					else
						Unit:Kill(true, -1)
				end
				--Alert Human player of the General Winter's effect
				local activePlayer = Players[Game.GetActivePlayer()];
				if activePlayer:IsHuman() then
					local header = Locale.ConvertTextKey("TXT_KEY_GENERAL_WINTER_NOTIFICATION_HEADER", playerWithKremlin:GetName());
					local message = Locale.ConvertTextKey("TXT_KEY_GENERAL_WINTER_NOTIFICATION_BODY", playerWithKremlin:GetName());
					print(message);					
					activePlayer:AddNotification(NotificationTypes.NOTIFICATION_GENERIC, message, header, iX, iY);
				end
			end							
		end
	end
end
		

GameEvents.PlayerDoTurn.Add(KremlinGeneralWinter)

I have been experimenting with it a bit more and here is what I have now, but I still am having trouble getting it to work.

Looking at the logs, the error is at line 37:
Code:
if unit ~= nil and playerWithKremlin.isAtWar(unit.getOwner()) then
lua:37: attempt to call field 'getOwner' (a nil value)

Removing that troublesome line just to see what would happen (and changing the way the damage is dealt since I was having trouble with it working) I come up with
Code:
--Figure out who has the Kremlin
function GetPlayerWithKremlin() 	
	local playerWithKremlin = nil;
	local buildingClassId = GameInfo.BuildingClasses["BUILDINGCLASS_KREMLIN"].ID;	
	
	for playerNum = 0, GameDefines.MAX_CIV_PLAYERS - 1 do
		local player = Players[playerNum];
		local buildingCount = player:GetBuildingClassCount(buildingClassId);
		if buildingCount > 0 then
			playerWithKremlin = player;
			break;
		end
	end	
		
	return playerWithKremlin
end

gGeneralWinterDamage = 10;

--General Winter Strikes
function KremlinGeneralWinter(iPlayer, iUnitID, iX, iY)
	if iPlayer ~= nil then
		local playerWithKremlin = GetPlayerWithKremlin();				
		
		--If the Kremlin has not yet been built
		if playerWithKremlin == nil then
			--print("The Kremlin does not exist yet");
			return;
		end

		local unit = Players[ iPlayer ]:GetUnitByID( iUnitID );
		
		--To check if the player is at war with Kremlin Owner
		if unit ~= nil then
			local plot = unit:GetPlot();			
						
			if plot:GetOwner() == playerWithKremlin:GetID() then
				--Time for General Winter Damage				
				local hitPoints = unit:GetCurrHitPoints();
				local damage = unit:GetDamage();					
				unit:SetDamage(damage + gGeneralWinterDamage);

				--Alert Human player of the General Winter's effect
				local activePlayer = Players[Game.GetActivePlayer()];
				if activePlayer:IsHuman() then
					local header = Locale.ConvertTextKey("TXT_KEY_GENERAL_WINTER_NOTIFICATION_HEADER", playerWithKremlin:GetName());
					local message = Locale.ConvertTextKey("TXT_KEY_GENERAL_WINTER_NOTIFICATION_BODY", playerWithKremlin:GetName());
					print(message);					
					activePlayer:AddNotification(NotificationTypes.NOTIFICATION_GENERIC, message, header, iX, iY);
				end
			end							
		end
	end
end
		

GameEvents.PlayerDoTurn.Add(KremlinGeneralWinter)

Now the issue with the changes is that it harms units (friendly and not) in the borders and, for some reason, only hits a single unit with the damage. I had 4 units inside the borders, yet only the "first" was harmed.

So I guess the primary issues I am having is how to limit the damage to only those at war with and expand it to all enemy units, instead of just the first.

And for my third attempt, because three times is apparently not the charm, I decided to try and separate each variable in its own function... With little success:
Spoiler :
Code:
--Figure out who has the Kremlin
function GetPlayerWithKremlin() 	
	local playerWithKremlin = nil;
	local buildingClassId = GameInfo.BuildingClasses["BUILDINGCLASS_KREMLIN"].ID;
	if playerWithKremlin == nil then
		for playerID = 0, GameDefines.MAX_CIV_PLAYERS - 1 do
			local player = Players[playerID];
			if (player:IsAlive() and player:GetCivilizationType() == civilisationID) then
				local buildingCount = player:GetBuildingClassCount(buildingClassId);
				if buildingCount > 0 then
					playerWithKremlin = playerID;
					break;
				end
			end
		end
	end
		
	return playerWithKremlin
end

--Time to check for war
local playerWithKremlin = GetPlayerWithKremlin();

function GeneralWinter_IsPlayerAtWarWithKremlin(playerID)
	local isAtWarwithKremlin = false
	if playerID ~= playerWithKremlin then
		local player = Players[playerID]
		local playerKremlinOwner = Players[playerWithKremlin]
		if Teams[player:GetTeam()]:IsAtWar(playerKremlinOwner:GetTeam()) then
			isAtWarWithKremlin = true
		end

		return isAtWarWithKremlin
	end
end

--Are you in Kremlin lands?
function GeneralWinter_IsthePlotKremlinOwned(Plot)
	local isPlotKremlinOwned = false
	if plot:GetOwner() > -1 then
		local player = Players[plot:GetOwner()]
		if player == playerWithKremlin then
			isPlotKremlinOwned = true
		end
	end

	return isPlotKremlinOwned
end

gGeneralWinterDamage = 10;

--General Winter
function KremlinGeneralWinter(playerID, unitID, unitX, unitY)
	local player = Players[playerID]
	if (player:IsAlive() and GeneralWinter_IsPlayerAtWarWithKremlin(playerID)) then
		local isPlotKremlinOwned = false
		local unit = player:GetUnitByID(unitID)
		local plot = unit:GetPlot()
		if (plot and (plot:GetOwner() == playerWithKremlin)) then
			isPlotKremlinOwned = true
		end

		if isPlotKremlinOwned then
			--Time for General Winter Damage				
			local hitPoints = unit:GetCurrHitPoints();
			local damage = unit:GetDamage();					
			unit:SetDamage(damage + gGeneralWinterDamage);

			--Alert Human player of the General Winter's effect
			local activePlayer = Players[Game.GetActivePlayer()];
			if activePlayer:IsHuman() then
				local header = Locale.ConvertTextKey("TXT_KEY_GENERAL_WINTER_NOTIFICATION_HEADER", playerWithKremlin:GetName());
				local message = Locale.ConvertTextKey("TXT_KEY_GENERAL_WINTER_NOTIFICATION_BODY", playerWithKremlin:GetName());
				print(message);					
				activePlayer:AddNotification(NotificationTypes.NOTIFICATION_GENERIC, message, header, iX, iY);
			end
		end
	end
end

GameEvents.PlayerDoTurn.Add(KremlinGeneralWinter)
Firetuner returns a runtime error at line 34: attempt to index 'playerKremlinOwner' (a nil value)
I'm not sure where it isn't defined :/


Any help with this would be greatly appreciated. As would any that point out issues with the current code that I have started.
 
You're pretty close. I'm not an expert by any stretch, but I might be able to help.

Let's look at try #2, since you indicate you had the most success with it.
The problem that the game complained about with line 37 was simple: it's GetOwner(), not getOwner() -- capitalization matters.

It looks like you figured out in #3 the other problem; that you need to check the player's team (rather than just the player) for at-war status.

So you need another loop to go through all of the at-war player's units (or I suppose you could loop through the plots of the Kremlin player's territory instead). Anyway, the former method would use:
Code:
for unit in Players[playerID]:Units() do

Is that enough to get you started?
 
Try this:

Spoiler :
Code:
local iGeneralWinterDamage = 10
local eKremlin = GameInfoTypes["BUILDING_KREMLIN"] --or whatever is the name of the building in-game

function KremlinGeneralWinter(iPlayer)
	local pPlayer = Players[iPlayer]
	local bHasKremlin = false
	for pCity in pPlayer:Cities() do
		if pCity:IsHasBuilding(eKremlin) then
			bHasKremlin = true
		end
	end
	if bHasKremlin then
		for i = 0, GameDefines.MAX_MAJOR_CIVS - 1, 1 do
			local pOtPlayer = Players[i]			
			if pOtPlayer:IsAlive() then
				local pOtTeam = Teams[pOtPlayer:GetTeam()]
				if pOtTeam:IsAtWar(pPlayer:GetTeam()) then
					for pUnit in pOtPlayer:Units() do
						local pPlot = pUnit:GetPlot()
						if pPlot:GetOwner() == iPlayer then
							pUnit:ChangeDamage(iGeneralWinterDamage)
						end
					end
				end
			end
		end
	end
end

GameEvents.PlayerDoTurn.Add(KremlinGeneralWinter)

It is not optimized for speed.
 
His original method of getting the building-class count will actually streamline things a little since he doesn't care which city has the wonder, only which player:
Spoiler :

Code:
local iGeneralWinterDamage = 10
iBuildingClassId = GameInfoTypes.BUILDINGCLASS_KREMLIN

function KremlinGeneralWinter(iPlayer)
	local pPlayer = Players[iPlayer]
	local bHasKremlin = false
	if pPlayer:GetBuildingClassCount(iBuildingClassId) > 0 then
		bHasKremlin = true
	end
	if bHasKremlin then
		for i = 0, GameDefines.MAX_MAJOR_CIVS - 1, 1 do
			local pOtPlayer = Players[i]			
			if pOtPlayer:IsAlive() then
				local pOtTeam = Teams[pOtPlayer:GetTeam()]
				if pOtTeam:IsAtWar(pPlayer:GetTeam()) then
					for pUnit in pOtPlayer:Units() do
						local pPlot = pUnit:GetPlot()
						if pPlot:GetOwner() == iPlayer then
							pUnit:ChangeDamage(iGeneralWinterDamage)
						end
					end
				end
			end
		end
	end
end
GameEvents.PlayerDoTurn.Add(KremlinGeneralWinter)
 
This seems very similar to what I do within the Lua I've written for the Shana Civ, wherein her Fuzetsu UB damages all enemy units around the city -- just that the starting point is different.

However, while the code provided above should largely be fine, there are a few issues I should note with them in their current state:
  • Due to the use of GameDefines.MAX_MAJOR_CIVS, this will not loop through, and thus will not affect, City-States, nor Barbarians. Use of GameDefines.MAX_PLAYERS will include them, although you will also have to iterate through many more 'inactive' entries.
  • The code above loops through every unit, and will affect both combat and civilian units alike. Use of pUnit:IsCombatUnit() may help limit that, otherwise Workers, Great People, etc. will be affected by the damage.

I should note that Shana does this in the "opposite" direction -- looping through all of her Cities, and scanning plots around those Cities looking for enemy units. However, this is largely because the range is limited to directly around the Cities. For something as expansive as "all of a given player's territory," I feel it makes more sense to scan through the players list and then loop through each of their units to see if any of them land inside the target Civ's territory.
 
Thanks everyone! I greatly appreciate all your help.

[*]Due to the use of GameDefines.MAX_MAJOR_CIVS, this will not loop through, and thus will not affect, City-States, nor Barbarians. Use of GameDefines.MAX_PLAYERS will include them, although you will also have to iterate through many more 'inactive' entries.
[*]The code above loops through every unit, and will affect both combat and civilian units alike. Use of pUnit:IsCombatUnit() may help limit that, otherwise Workers, Great People, etc. will be affected by the damage.
[/LIST]

I hadn't considered the non-combat part of it. No need to kill captured civilians or Great Generals lurking about. I also hadn't considered city-states or barbarians, but given their army size, I think I'll keep them immune. For some explanation in a role-play context, I guess it would make sense for barbs and nearby city-states to know how to handle the terrain.

Nutty said:
it's GetOwner(), not getOwner() -- capitalization matters.

That is definitely a problem I am still having while learning my way around Lua, capitals and the little other nuances. Thanks.

And of course thank you for bane and LeeS for your contributions to helping me fix the code up.
 
The code above loops through every unit, and will affect both combat and civilian units alike. Use of pUnit:IsCombatUnit() may help limit that, otherwise Workers, Great People, etc. will be affected by the damage.

Note that pUnit:IsCombatUnit() returns false for any unit with a Combat value of 0 ... which includes aircraft and missiles

Not an issue in the current context, but something to generally be aware of
 
Back
Top Bottom