[WIP] Losing Wonder Race Awards Special Builder Unit

Ryoga

King
Joined
Oct 12, 2010
Messages
993
This is my first attempt at modding using a LUA script. So there are many things that I'm not sure how they work.

As the title implies this mod is supposed to automatically give a special unit (a sort of mini Engineer, whose properties are yet to be defined) to any city that just saw its wonder production halted because someone else completed it first.


As far as I know there are no hooks for a Wonder race lost so I devised a workaround by modifying Hambil's "Wonder Race" mod.

Currently the code is like this:

Code:
-- "gDoomedToFail" stores the number of cities (of any CIV) that will lose their Wonder next turn.
-- Following arrays will store the city owner and city position for each instance of "gDoomedToFail"
local gDoomedToFail = 0
local gLoserPlayer = {}
local gLoserCityPlotX = {}
local gLoserCityPlotY = {}


function DoTurn( playerID )
	local enemyTurnsLeft = 0
	local myTurnsLeft = 0
	local iUnitID
	local player = Players[playerID]
	local enemyPlayer
	local buildingInfo
	local buildingClassInfo
	local enemyBuildingInfo

	if not player then 
		return 
	end

	-- if gDoomedToFail is greater than 0 process everything on human player turn and then do nothing until next turn.
	if playerID == 0 then
		while gDoomedToFail > 0 do
			--Unit placeholder, will be changed later
			iUnitID = GameInfoTypes["UNIT_MISSIONARY"]
			gLoserPlayer[gDoomedToFail]:InitUnit (iUnitID, gLoserCityPlotX[gDoomedToFail], gLoserCityPlotY[gDoomedToFail])
			gDoomedToFail = gDoomedToFail - 1
		end
	end

	for city in player:Cities() do

		buildingInfo = GameInfo.Buildings[city:GetProductionBuilding()]

		if buildingInfo then
			buildingClassInfo = GameInfo.BuildingClasses[buildingInfo.BuildingClass]
			if buildingClassInfo.MaxGlobalInstances == 1 then 

				myTurnsLeft = city:GetProductionTurnsLeft()

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

					enemyPlayer = Players[iPlayer]
					
					if enemyPlayer ~= nil and enemyPlayer ~= player and enemyPlayer:IsAlive() then
						for enemyCity in enemyPlayer:Cities() do
						
							enemyBuildingInfo = GameInfo.Buildings[enemyCity:GetProductionBuilding()]
							if enemyBuildingInfo and enemyBuildingInfo == buildingInfo then
								enemyTurnsLeft = enemyCity:GetProductionTurnsLeft()
								--Will I lose the Race next turn? Only true if 1: Other Civ City is at 1 turn from completion.
								--2a: I won't complete the wonder next turn OR 2b: I will complete the wonder next turn but I'm either
								-- the human player or my turn will be processed after.
								if enemyTurnsLeft == 1 and ( myTurnsLeft > 1 or ( myTurnsLeft == 1 and (PlayerID > iPlayer or PlayerID == 0)))  then
									gDoomedToFail = gDoomedToFail + 1
									gLoserPlayer[gDoomedToFail] = player
									gLoserCityPlotX[gDoomedToFail] = city:GetX()
									gLoserCityPlotY[gDoomedToFail] = city:GetY()
								end
								-- I confirmed that this player is building the wonder, he can't possibly be building it in another city
								-- Let's move to the next player
								iPlayer = iPlayer + 1
							end
						end
					end
				end	
			end
		end
	end
end

function DoInit()
	ContextPtr:SetHide(true)
end

GameEvents.PlayerDoTurn.Add( DoTurn );
Events.SequenceGameInitComplete.Add( DoInit );


As it stands now, the code seems to work as it should, but it is not quite easy to test every possible scenario.

I am particularly unsure about this part:

Code:
--Will I lose the Race next turn? Only true if 1: Other Civ City is at 1 turn from completion.
--2a: I won't complete the wonder next turn OR 2b: I will complete the wonder next turn but I'm either
-- the human player or my turn will be processed after.
if enemyTurnsLeft == 1 and ( myTurnsLeft > 1 or ( myTurnsLeft == 1 and (PlayerID > iPlayer or PlayerID == 0)))  then

It is easy to see who will win if one is 1 turn from completion and the other is not. A lot more ambiguous when both are.
I know (or so I think) that the human player will always lose in this case, but I'm not sure how it is handled for everyone else.
I am assuming here that PlayerID is a number comprised between "0" (the human player) and the total number of CIVs at the start of the game and the same is true for iPlayer. And I'm assuming that whoever has the lower PlayerID will be winning the wonder race in case of a tie, but I can't be quite sure about that.


That apart at this point I would like to find a way to determine how much progress is lost at the time of the wonder race failure.

I know how to get the turns remaining, I could probably get the wonder base cost from Gameinfo, but how do I proceed from that?
I'm looking for suggestions.
 
made a few changes to the code in

Code:
	if playerID == 0 then
		while gDoomedToFail > 0 do
			--Unit placeholder, will be changed later
			iUnitID = GameInfoTypes["UNIT_MISSIONARY"]
			gLoserPlayer[gDoomedToFail]:InitUnit (iUnitID, gLoserCity[gDoomedToFail]:GetX(), gLoserCity[gDoomedToFail]:GetY())
			local gold = gLostProduction[gDoomedToFail] * -1
			gLoserPlayer[gDoomedToFail]:ChangeGold(gold)
			gDoomedToFail = gDoomedToFail - 1
		end
	end

and

Code:
if enemyBuildingInfo and enemyBuildingInfo == buildingInfo then
								enemyTurnsLeft = enemyCity:GetProductionTurnsLeft()
								--Will I lose the Race next turn? Only true if 1: Other Civ City is at 1 turn from completion.
								--2a: I won't complete the wonder next turn OR 2b: I will complete the wonder next turn but I'm either
								-- the human player or my turn will be processed after.
								if enemyTurnsLeft == 1 and ( myTurnsLeft > 1 or ( myTurnsLeft == 1 and (PlayerID > iPlayer or PlayerID == 0)))  then
									gDoomedToFail = gDoomedToFail + 1
									gLoserPlayer[gDoomedToFail] = player
									gLoserCity[gDoomedToFail] = city
									gLostProduction[gDoomedToFail] = city:GetBuildingProduction(building)
								end
								-- I confirmed that this player is building the wonder, he can't possibly be building it in another city
								-- Let's move to the next player
								iPlayer = iPlayer + 1
							end

It works incredibly well. This effectively cancels out the exact money reward you get for losing a wonder race (needed since with this mod you get a different kind of compensation).

Now the main issue is to find a way to make that production lost come back as bonus production of the same amount.

I tried with

Code:
gLoserCity[gDoomedToFail]:SetOverflowProduction(gLostProduction[gDoomedToFail])

However this doesn't seem to work. I actually have no idea how this overflowproduction work anyway.

So going back to my original idea, I can create a unit similar to an engineer which can be spent to increase the production of a wonder.

The relevant variable are:

Code:
<BaseHurry>300</BaseHurry>
<HurryMultiplier>30</HurryMultiplier>

According to some people experiment, that "300" of "basehurry" corresponds to exactly 300 hammers (production) at normal speed. "Hurrymultiplier" is an added bonus dependent on the city's population. In theory by setting it to 0, the unit should give 300 hammers at normal speed.

If there isn't a way to assign a variable "basehurry" to units then I can only think of creating multiple units each with different "basehurry", but that could be a lot of them when you consider a wonder cost can go up to 1060.
 
All right perhaps someone can help me with a simple issue...

I need to retrieve the value of "UnitHurryPercent" as specified in "CIV5GameSpeeds.xml". However I need the one specific of the game speed that the game is currently on.
How do I do that?

EDIT:

Nevermind, don't bother, I found it:

Code:
GameInfo.GameSpeeds[ PreGame.GetGameSpeed() ].UnitHurryPercent
 
Okay after many tries and testings I am finally ready to release and alpha version of my mod.

I found a better way to obtain what I wanted without using units at all (the idea became obsolete at the time I realized that the AI can't manage them well).

So what does the mod do now?
Whenever a wonder is lost instead of getting the lost hammers back as gold, the amount of hammers lost is stored and assigned to whatever wonder you will build or are currently building.

So let's say you are building Chichen Itza and you are at 200/300 :c5production:
You press "next turn" and when it's done BAM you get the message that some other CIV completed it first.
Usually you'd get 200 gold as compensation, but with this mod you don't. Instead when you put in queue another wonder, let's say "Notre Dame", at the start of your next turn you'll have 200 :c5production: in addition to whatever hammers your city produced that turn!

In case you are already building "Notre Dame" in another city, it automatically gets the lost hammers.

And in case you are already 300/400 :c5production: on "Notre Dame", only 100 :c5production: of what you had previous lost are used for it so you go to 400/400 :c5production: . The remaining 100 are stored and will be given to the next wonder you'll put in queue in any of your cities.

Nothing gets wasted!


Naturally other civs get the very same treatment. So if you beat them to a wonder, they'll probably get another one very soon.
This mod makes wonder hogging very difficult, this is a wanted effect. You are enforced to choose the wonder you want to build wisely.


Known issues:

-The mod is based on a check that is made on a "turn before prediction". Whenever someone is 1 turn before the completion of a wonder (and other conditions) the mod assumes that he will win the race next turn and whoever is competing for it will lose.
Supposing a Civ for whatever reasons jumps from 2 turns remaining to 0 (possible in case of a sudden increase of hammers), the check fails. However even in this case the losing player still gets the normal reward (Engineer and hammer recovery from this mod always bring the turn remaining to 1, so they never cause a problem).
Worse yet is the case of someone that is supposed to win but he doesn't (because of a decrease of hammers). Those who were supposed to lose might even win and get bonus hammers for another wonder too, while the real loser just gets the normal gold reward (this is extremely rare but can happen).

-This mod is exploitable. If you know that an enemy is building a wonder that you don't want (through a spy for example), you can decide to put it in queue for the purpose of losing the race and save the hammers for a better wonder that you have yet to unlock.
I could create an hammer decay routine, but that would put the AI at a disadvantage since it isn't programmed to promptly make use of the saved hammers.


I would be very happy if anyone could test my mod and\or give their opinions about it.
File is attached to this post.
 
A lot better code.
The check now should be 100% reliable.

Code:
-- Lua Script
-- Author: Ryoga
-- DateCreated: 9/25/2014 5:49:47 PM
--------------------------------------------------------------

-- "gWonder" stores the number of cities (per Civ) that are building wonder.
-- It is checked and resetted every turn.
-- "gProductionLost" stores the cumulative hammers that have been lost by each Civ.
local gWonder = {}
local gProduction = {}
local gProductionLost = {}
local gBuilding = {}


function DoTurn( playerID )
	local player = Players[playerID]
	local enemyPlayer
	local slot
	local gold
	local building
	local buildingInfo
	local buildingClassInfo

	if not player then 
		return 
	end

	--Make sure that these arrays' needed values aren't zero
	if not gWonder[playerID] then
		gWonder[playerID] = 0
	end
	if not gProductionLost[playerID] then
		gProductionLost[playerID] = 0
	end

	--I was building these wonders the turn before, has anybody else completed it now?
	while gWonder[playerID] > 0 do
		for iPlayer = 0, GameDefines.MAX_MAJOR_CIVS - 1, 1 do

			enemyPlayer = Players[iPlayer]
			--make sure that data are stored in different slots for each Civ.
			slot = gWonder[playerID] + (playerID * 20)

			if enemyPlayer ~= nil and enemyPlayer ~= player and enemyPlayer:IsAlive() then
				for enemyCity in enemyPlayer:Cities() do
					if enemyCity:IsHasBuilding(gBuilding[slot]) then
						--remove the gold compensation
						gold = gProduction[slot] * -1
						player:ChangeGold(gold)
						gProductionLost[playerID] = gProductionLost[playerID] + gProduction[slot]
					end
				end
			end
		end
		gWonder[playerID] = gWonder[playerID] - 1
	end

	for city in player:Cities() do
		
		building = city:GetProductionBuilding()
		buildingInfo = GameInfo.Buildings[city:GetProductionBuilding()]

		if buildingInfo then
			buildingClassInfo = GameInfo.BuildingClasses[buildingInfo.BuildingClass]
			if buildingClassInfo.MaxGlobalInstances == 1 then
	
				gWonder[playerID] = gWonder[playerID] + 1
				slot = gWonder[playerID] + (playerID * 20)
				gProduction[slot] = city:GetBuildingProduction(building)
				gBuilding[slot] = building

				if gProductionLost[playerID] > 0 then
					local production = gProductionLost[playerID]
					local productionNeeded = player:GetBuildingProductionNeeded(building)
					local produced = city:GetBuildingProduction(building)
					-- add production recovered to production already existent
					production = production + produced
					-- Is it more than what is needed to build the wonder?
					if production > productionNeeded then
						-- use only what's needed and store the remainder for later use.
						gProductionLost[playerID] = production - productionNeeded
						production = productionNeeded
					else
						gProductionLost[playerID] = 0
					end
					city:SetBuildingProduction(building, production)
				end
			end
		end
	end
end

function DoInit()
	ContextPtr:SetHide(true)
end

GameEvents.PlayerDoTurn.Add( DoTurn );
Events.SequenceGameInitComplete.Add( DoInit );
 
I haven't realy dug into your code yet, but unless you are persisting the data using SaveUtils.lua or something similar, when a player has 200 hammers "stored" upon save-game, the stored amount of hammers would be lost on re-loading the saved game, I would think.
 
Yeah I imagined that would be the case.
I have no idea how that works though...

Any hint on how I can make the data persistent or can you point me to some kind of reference?
 
Thank you for the links LeeS, I've been trying to make use of Why's "SaveUtils.lua" file but without any success so far.
I don't quite know what I'm doing wrong.

-I have copied and included SaveUtils.lua in my project.
-I set its "import into VFS" to true
-I have changed the the starter code as follow:

Code:
-- Lua Script1
-- Author: Mark
-- DateCreated: 4/17/2013 2:01:35 AM
--------------------------------------------------------------
include("WonderRaceLost.lua")
[COLOR="Red"]include( "SaveUtils" ) MY_MOD_NAME = "Wonder Race Lost - Gives Hammers Back!"[/COLOR]

-- Default show/hide call
ContextPtr:SetShowHideHandler(function (bIsHide, bInitState)
 
end)

-- Player clicked Close button
Controls.Close:RegisterCallback(Mouse.eLClick, function () ContextPtr:SetHide(true) end)

Then I changed the main code this way:

Code:
-- Lua Script
-- Author: Ryoga
-- DateCreated: 9/25/2014 5:49:47 PM
--------------------------------------------------------------

-- "gWonder" stores the number of cities (per Civ) that are building wonder.
-- It is checked and resetted every turn.
-- "gProductionLost" stores the cumulative hammers that have been lost by each Civ.
local gWonder = {}
local gProduction = {}
local gProductionLost = {}
local gBuilding = {}


function DoTurn( playerID )
	local player = Players[playerID]
	local enemyPlayer
	local slot
	local gold
	local building
	local buildingInfo
	local buildingClassInfo

	if not player then 
		return 
	end

	--Make sure that these arrays' needed values aren't zero
	if not gWonder[playerID] then
		gWonder[playerID] = 0
	end
	if not gProductionLost[playerID] then
		gProductionLost[playerID] = 0
	end

	--I was building these wonders the turn before, has anybody else completed it now?
	while gWonder[playerID] > 0 do
		for iPlayer = 0, GameDefines.MAX_MAJOR_CIVS - 1, 1 do

			enemyPlayer = Players[iPlayer]
			--make sure that data are stored in different slots for each Civ.
			slot = gWonder[playerID] + (playerID * 20)

			if enemyPlayer ~= nil and enemyPlayer ~= player and enemyPlayer:IsAlive() then
				for enemyCity in enemyPlayer:Cities() do
					if enemyCity:IsHasBuilding(gBuilding[slot]) then
						--remove the gold compensation
						gold = gProduction[slot] * -1
						player:ChangeGold(gold)
						gProductionLost[playerID] = gProductionLost[playerID] + gProduction[slot]
						[COLOR="Red"]save( playerID, "productionLost", gProductionLost[playerID] )[/COLOR]
					end
				end
			end
		end
		gWonder[playerID] = gWonder[playerID] - 1
	end

	for city in player:Cities() do
		
		building = city:GetProductionBuilding()
		buildingInfo = GameInfo.Buildings[city:GetProductionBuilding()]

		if buildingInfo then
			buildingClassInfo = GameInfo.BuildingClasses[buildingInfo.BuildingClass]
			if buildingClassInfo.MaxGlobalInstances == 1 then
	
				gWonder[playerID] = gWonder[playerID] + 1
				slot = gWonder[playerID] + (playerID * 20)
				gProduction[slot] = city:GetBuildingProduction(building)
				gBuilding[slot] = building

				if gProductionLost[playerID] > 0 then
					local production = gProductionLost[playerID]
					local productionNeeded = player:GetBuildingProductionNeeded(building)
					local produced = city:GetBuildingProduction(building)
					-- add production recovered to production already existent
					production = production + produced
					-- Is it more than what is needed to build the wonder?
					if production > productionNeeded then
						-- use only what's needed and store the remainder for later use.
						gProductionLost[playerID] = production - productionNeeded
						production = productionNeeded
					else
						gProductionLost[playerID] = 0
					end
					city:SetBuildingProduction(building, production)
					[COLOR="Red"]save( playerID, "productionLost", gProductionLost[playerID] )[/COLOR]
				end
			end
		end
	end
end

function DoInit()
	ContextPtr:SetHide(true)
end

[COLOR="Red"]function DoRetrieveData()
	for iPlayer = 0, GameDefines.MAX_MAJOR_CIVS - 1, 1 do
		player = players[iPlayer]
		if player ~= nil and player:IsAlive() then
			gProductionLost[iPlayer] = load( iPlayer, "productionLost" ) or 0
		end
	end
end[/COLOR]

GameEvents.PlayerDoTurn.Add( DoTurn );
Events.SequenceGameInitComplete.Add( DoInit );
[COLOR="Red"]Events.LoadScreenClose.Add( DoRetrieveData );[/COLOR]


Now I don't know if the whole function "DoRretrieveData" makes sense or not. The problem is that even if I disable it (and the "events.LoadScreenClose" line as well), the mere fact of adding the "save" lines in the code, messes up the mod, and the wonders aren't given the lost production AT ALL.
And if I disable the "save" lines everything works as usual.

I don't know how could that happen, I'm pretty sure the code isn't busted, in fact it actually creates some strange effects (like removing gold, but more than it should).

What am I doing wrong?
Please help!
 
One problem I see is with player = players[iPlayer]. I'm pretty sure the game wants Players instead of players. I think you mis-typed because you have it as Players elsewhere.
Code:
function DoRetrieveData()
	for iPlayer = 0, GameDefines.MAX_MAJOR_CIVS - 1, 1 do
		player = [COLOR="Blue"]P[/COLOR]layers[iPlayer]
		if player ~= nil and player:IsAlive() then
			gProductionLost[iPlayer] = load( iPlayer, "productionLost" ) or 0
		end
	end
end
There also might be a problem with using the load and the save command in two different functions. I know however that this will not be a problem if you use the TableSaverLoader instead of the SaveUtils. And since you are using gProductionLost as a table within lua, "TableSaverLoader.lua" might be the better choice. Second link in my post about utilities for persisting data across saved games.
 
Thanks, it seems that I managed to make it work now.

Code:
-- Lua Script
-- Author: Ryoga
-- DateCreated: 9/25/2014 5:49:47 PM
--------------------------------------------------------------

-- "gWonder" stores the number of cities (per Civ) that are building wonder.
-- It is checked and resetted every turn.
-- "gProductionLost" stores the cumulative hammers that have been lost by each Civ.

include( "SaveUtils" ); MY_MOD_NAME = "Wonder Race Lost - Gives Hammers Back!";

local gWonder = {}
local gProduction = {}
--local gProductionLost = {}
local gBuilding = {}


function DoTurn( playerID )
	local player = Players[playerID]
	local enemyPlayer
	local slot
	local gold
	local building
	local buildingInfo
	local buildingClassInfo
	local productionLost

	if not player then 
		return 
	end

	if not gWonder[playerID] then
		gWonder[playerID] = 0
	end

	--I was building these wonders the turn before, has anybody else completed it now?
	while gWonder[playerID] > 0 do
		for iPlayer = 0, GameDefines.MAX_MAJOR_CIVS - 1, 1 do

			enemyPlayer = Players[iPlayer]
			--make sure that data are stored in different slots for each Civ.
			slot = gWonder[playerID] + (playerID * 20)

			if enemyPlayer ~= nil and enemyPlayer ~= player and enemyPlayer:IsAlive() then
				for enemyCity in enemyPlayer:Cities() do
					if enemyCity:IsHasBuilding(gBuilding[slot]) then
						--remove the gold compensation
						gold = gProduction[slot] * -1
						player:ChangeGold(gold)
						productionLost = load( player, "productionLost" ) or 0
						productionLost = productionLost + gProduction[slot]
						save( player, "productionLost", productionLost )
						print( "Saved Production Lost for player " .. tostring( playerID ) .. ": " .. tostring( productionLost ) )
					end
				end
			end
		end
		gWonder[playerID] = gWonder[playerID] - 1
	end

	for city in player:Cities() do
		
		building = city:GetProductionBuilding()
		buildingInfo = GameInfo.Buildings[city:GetProductionBuilding()]

		if buildingInfo then
			buildingClassInfo = GameInfo.BuildingClasses[buildingInfo.BuildingClass]
			if buildingClassInfo.MaxGlobalInstances == 1 then
	
				gWonder[playerID] = gWonder[playerID] + 1
				slot = gWonder[playerID] + (playerID * 20)
				gProduction[slot] = city:GetBuildingProduction(building)
				gBuilding[slot] = building
				productionLost = load( player, "productionLost" ) or 0

				if productionLost > 0 then
					local production = productionLost
					local productionNeeded = player:GetBuildingProductionNeeded(building)
					local produced = city:GetBuildingProduction(building)
					-- add production recovered to production already existent
					production = production + produced
					-- Is it more than what is needed to build the wonder?
					if production > productionNeeded then
						-- use only what's needed and store the remainder for later use.
						productionLost = production - productionNeeded
						production = productionNeeded
					else
						productionLost = 0
					end
					city:SetBuildingProduction(building, production)
					print( "Player" .. tostring( playerID ) .. " has recovered production for a wonder!" ) )
					save( player, "productionLost", productionLost )
				end
			end
		end
	end
end

function DoInit()
	ContextPtr:SetHide(true)
end


GameEvents.PlayerDoTurn.Add( DoTurn );
Events.SequenceGameInitComplete.Add( DoInit );

There were several different errors that I managed to spot thanks to live tuner.
I made things a lot more simple. Since "productionLost" is already stored already in a table by saveUtils, i no longer need to have it as a table, so all that I'm using SaveUtils for is a single numeric value for each player and nothing else.

Testing gave positive result finally, even after leaving the game and reentering the new wonder gets the production lost.

I have just a few doubts.
basically now the script accesses the saved data with "load" for each turn each wonder in the world is being built. Is that a problem?

Live tuner also frequently reports:

"WonderRaceLostDialog: Warning: cache not shared."

which apparently is a printed line from SaveUtils.
What does that mean?
I must admit that I don't quite understand what "cache" refers to, and what does it mean that is not "shared".
 
All right after several testing I finally learned how turns work and how "GameEvents.PlayerDoTurn.Add()" fit in.

A turn starts with "player0" (human player usually) and does all the automated stuff, then it does whatever "GameEvents.PlayerDoTurn.Add()" specifies for player0, at this point the routine pauses so that the player can do all his moves and decisions. but his turn for what concerns the computer is already processed.
After pressing "next turn", the routine resume with player1, it does all the automated stuff and then executes whatever is in "GameEvents.PlayerDoTurn.Add()". This sequence repeats for every player until it gets back to the human player (player0), it processes his turn again and only then it pauses again so that the player can do his stuff.

With this new knowledge I managed to refine my code again and now it should work perfectly.


I attached the latest version of the mod for whoever wants to try it.
 

Attachments

  • Wonder Race Lost - Gives Hammers Back! (v 4).civ5mod
    8.5 KB · Views: 54
Top Bottom