Lua code for National Wonder terrain req

Cladoniaceae

Chieftain
Joined
Feb 25, 2017
Messages
12
I'm trying to write a Lua script that will place a certain dummy building only in cities on or near Tundra when they are built, with the ultimate goal of making a National Wonder that requires all cities to be built on or near Tundra. What I have right now is based on a couple of examples put together, but it's not working. Any ideas what I'm doing wrong?

Code:
local DummyB = GameInfoTypes.BUILDING_DUMMY_TURF_HALL_REQ

GameEvents.PlayerCityFounded.Add(function(iPlayer, iCityX, iCityY)
    local kplot = Map.GetPlot(iCityX, iCityY);
    local kCity = kplot:GetPlotCity();
    CheckNewCityTundra(kCity, true)
end)

function CheckNewCityTundra(iPlayer)
    local pPlayer = Players[iPlayer]
        for i = 0, kCity:GetNumCityPlots() - 1, 1 do
            local pPlot = kCity:GetCityIndexPlot(i)
            if pPlot:GetTerrainType() == TerrainTypes.TERRAIN_TUNDRA then
                kCity:SetNumRealBuilding(DummyB, 1);
            else
                kCity:SetNumRealBuilding(DummyB, 0);
            end
        end
    end
end
 
Problems like these can be solved using the Lua.log as you'll get runtime errors there!

-----

Now for your actual issue:
First of all, you have an extra 'end'. Remove the penultimate 'end' as (with the current indents) it does not suggest that it should be there.
Secondly, you call this:
Code:
CheckNewCityTundra(kCity,true)
whereas you defined the function as this:
Code:
function CheckNewCityTundra(iPlayer)
There are two problems with this:
1) You call the function using two parameters (kCity and true), while in fact you only should call one (iPlayer)
2) You pass kCity as a parameter to CheckNewCityTundra, while you should pass iPlayer as you defined it like that

Also, most modders use a thing called Hungarian Notation, which causes those letters to be placed in front of variable names. Here's a quick rundown: sCity is used for a variable that contains a string, probably a name of the City as that's what suggested by the variable-name. pPlayer is the pointer towards a Player object, which can be used for functions such as pPlayer:GetName() and such. iPlayer is an integer, usually a player ID as that's also an integer. We could also have bCityHasTundra, which is a boolean. Lastly, there's tCity, which means a table of cities. In general, the variable-name describes what is stored (something related to cities, players, units, etc.), and the extra letter describes what type of data is stored (boolean, integer, table, etc.)

;tldr Use pPlot instead of kplot, and use pCity instead of kCity

Furthermore, there seems to be a logical error in your code. For every plot, if it is not tundra, we remove the dummy from the city. This means that, if the last plot we check is not a tundra, while all other plots are tundra, we do not get the dummy! In fact, since you only run this function once (when the city is founded), you can actually just remove the whole line which states 'pCity:SetNumRealBuilding(iDummyB, 0)', as the city won't have this building upon being founded anyhow. (I left it in in the code below if you do not want to remove it for some reason)

I also suggest you add some print()s (which show up in the Lua.log), and allow for easier debugging

Lastly, you seem to use two notations throughout each other. While that's perfectly fine, it makes it less clear. I suggest using one of the following methods:
Code:
local iDummyB = GameInfoTypes.BUILDING_DUMMY_TURF_HALL_REQ

GameEvents.PlayerCityFounded.Add(function (iPlayer,iCityX,iCityY)
    local pPlot = Map.GetPlot(iCityX,iCityY)
    local pCity = pPlot:GetPlotCity();
    print("Checking "..pCity:GetName())
  
    pCity:SetNumRealBuilding(iDummyB, 0);
  
    for i = 0, pCity:GetNumCityPlots() - 1, 1 do
        local pPlot = pCity:GetCityIndexPlot(i)
        if pPlot:GetTerrainType() == TerrainTypes.TERRAIN_TUNDRA then
            print("Tundra found at ("..pPlot:GetX()..","..pPlot:GetY()..")")
            pCity:SetNumRealBuilding(iDummyB, 1);
        end
    end
end)
or
Code:
local iDummyB = GameInfoTypes.BUILDING_DUMMY_TURF_HALL_REQ

function CheckNewCityTundra(iPlayer,iCityX,iCityY)
    local pPlot = Map.GetPlot(iCityX,iCityY)
    local pCity = pPlot:GetPlotCity();
    print("Checking "..pCity:GetName())
  
    pCity:SetNumRealBuilding(iDummyB, 0);
  
    for i = 0, pCity:GetNumCityPlots() - 1, 1 do
        local pPlot = pCity:GetCityIndexPlot(i)
        if pPlot:GetTerrainType() == TerrainTypes.TERRAIN_TUNDRA then
            print("Tundra found at ("..pPlot:GetX()..","..pPlot:GetY()..")")
            pCity:SetNumRealBuilding(iDummyB, 1);
        end
    end
end

GameEvents.PlayerCityFounded.Add(CheckNewCityTundra)

I prefer the latter as I personally find that clearer
 
What an incredibly (locally) helpful and (globally) informative reply! I think I have a better understanding of how Lua works now, so thank you.

I tried your code and it works, almost. It did successfully give cities the dummy, allowing the wonder to be built, but some of the cities I tested shouldn't have gotten it. Looking at the Lua.log. it appears that it returned positives for Tundra tiles outside cities' borders (but within workable, i.e. 3-hex, range). I'm not sure where to go from here in order to limit the range it uses to search for Tundra to 1 hex outside the city.

Another, more mysterious bug I'm encountering is that the wonder's civilopedia entry fails to load if I access it by right-clicking the build button. The entry is there if I look it up manually, but the text fields show up empty. I tried isolating the problem by first removing the Lua (and setting the dummy building to be automatically free in all cities)--which didn't solve the problem--and then by removing the dummy building and building requirements altogether--which did. It seems the dummy building code is interfering with the civilopedia loading somehow, but I'm stumped as to why, because everything else about the wonder is working as it should.

For some reason, I can't attach files to this post, but I'm providing Dropbox links to a txt version of the aforementioned the Lua.log and a copy of the mod in case you or anybody else would be willing to take a look.

Lua.log.txt
Turf Hall Test.civ5mod
 
'Nearby' has quite a broad definition in some sense ;)
But if it's only a 1 tile range then we can do something easier (and less laggy):
Code:
--pPlot was defined somewhere here
for i = 0, 5 do --loop through all adjacent plots
        local pPlotAdj = Map.PlotDirection(pPlot:GetX(),pPlot:GetY(),i);
        print("Checking plot: X="..pPlotAdj:GetX().." Y="..pPlotAdj:GetY())
        if pPlotAdj and pPlotAdj:GetTerrainType() == iTundra then
            print("Tundra found at ("..pPlotAdj:GetX()..","..pPlotAdj:GetY()..")")
            --do stuff here
        end
    end

Also, we can further optimize (as in, reduce lag) the previous code, as I noticed that it also fires for City States and the Barbarian Player (which cannot build (national) wonders anyhow)
This only needs a simple extra check:
Code:
if not pPlayer:IsMinorCiv() and not pPlayer:IsBarbarian() then

Also, we know enough if we find just one tundra tile nearby, meaning that we could actually stop checking. This can be done the easiest using a separate function, and calling that in the main code.
Code:
[..]snip[..]
--this is the main code

--we call the function here, which returns true as soon as it has found a tundra plot
if IsAdjacentToOrOnTundra(pPlot) then
[..]snip[..]

--returns true when a given plot is adjacent to or on a tundra. returns false otherwise; assumes pPlot ~= nil
function IsAdjacentToOrOnTundra(pPlot)
    if pPlot:GetTerrainType() == TerrainTypes.TERRAIN_TUNDRA then
        print("Tundra found at ("..pPlot:GetX()..","..pPlot:GetY()..")")
        return true
    end
    for i = 0, 5 do --loop through all adjacent plots
        local pPlotAdj = Map.PlotDirection(pPlot:GetX(),pPlot:GetY(),i);
        print("Checking plot: X="..pPlotAdj:GetX().." Y="..pPlotAdj:GetY())
        if pPlotAdj and pPlotAdj:GetTerrainType() == TerrainTypes.TERRAIN_TUNDRA then
            print("Tundra found at ("..pPlotAdj:GetX()..","..pPlotAdj:GetY()..")")
            return true
        end
    end
    print("No Tundra found at or adjacent to ("..pPlot:GetX()..","..pPlot:GetY()..")")
    return false
end

Lastly, for it's better to use variables for things such as TerrainTypes.TERRAIN_TUNDRA (or any GameInfoTypes.SOMETHING_SOMETHING-variant for that matter)
If we use a variable, we only need to check the database for TerrainTypes.TERRAIN_TUNDRA once, which is when we define the variable.
If we wouldn't use a variable, then every time we call TerrainTypes.TERRAIN_TUNDRA, we would need to check the database, which is way slower than just checking a variable.
Also, if you ever wish to change which terrain is valid, then all you need to do is change that one single variable, rather then searching the code for all the TerrainTypes.TERRAIN_TUNDRA and then accidentally missing one causing your code to not work properly anymore...
You can do this in the same way you did it for the dummy building.

Spoiler Full Code :

Code:
local iDummyB = GameInfoTypes.BUILDING_DUMMY_TURF_HALL_REQ
local iTundra = TerrainTypes.TERRAIN_TUNDRA

function UpdateTundraDummy (iPlayer,iCityX,iCityY)
    local pPlayer = Players[iPlayer]
    --extra check for minor civs and the barbarian player; they can't build national wonders anyhow
    if not pPlayer:IsMinorCiv() and not pPlayer:IsBarbarian() then
        local pPlot = Map.GetPlot(iCityX,iCityY)
        local pCity = pPlot:GetPlotCity();
        print("Checking "..pCity:GetName().." of "..pPlayer:GetName())
   
        --using a seperate function allows us to terminate the loop earlier; reducing lag in the process
        if IsAdjacentToOrOnTundra(pPlot) then
            pCity:SetNumRealBuilding(iDummyB, 1);
            print("Dummy building for "..pCity:GetName())
        else
            pCity:SetNumRealBuilding(iDummyB, 0);
            print("No dummmy building for "..pCity:GetName())
        end
    end
end


function IsAdjacentToOrOnTundra(pPlot)
    if pPlot:GetTerrainType() == iTundra then
        print("Tundra found at ("..pPlot:GetX()..","..pPlot:GetY()..")")
        return true
    end
    for i = 0, 5 do --loop through all adjacent plots
        local pPlotAdj = Map.PlotDirection(pPlot:GetX(),pPlot:GetY(),i);
        print("Checking plot: X="..pPlotAdj:GetX().." Y="..pPlotAdj:GetY())
        if pPlotAdj and pPlotAdj:GetTerrainType() == iTundra then
            print("Tundra found at ("..pPlotAdj:GetX()..","..pPlotAdj:GetY()..")")
            return true
        end
    end
    print("No Tundra found at or adjacent to ("..pPlot:GetX()..","..pPlot:GetY()..")")
    return false
end

GameEvents.PlayerCityFounded.Add(UpdateTundraDummy)


--------
Regarding your civilopedia-issue, try checking Database.log and xml.log first.

EDIT: It might be related due to the fact that it might not be able to display the required dummy in the civilopedia (due to it having <GreatWorkCount>-1</GreatWorkCount>) Not sure if that could actually be the issue
 
Last edited:
'Nearby' has quite a broad definition in some sense ;)
You'll have to take that up with Firaxis and their XML tag <NearbyTerrainRequired>! :p

But onto your code: it's beautiful! Seriously, I'm vomiting rainbows over here. You were right about the civilopedia issue, not because of the Great Work count (I don't think), but rather because it wanted to show a required building for the wonder but didn't know what to call it. Solution: I gave the dummy building the following qualities
Code:
            <PortraitIndex>16</PortraitIndex>
            <IconAtlas>TERRAIN_ATLAS</IconAtlas>
            <Description>TXT_KEY_TERRAIN_TUNDRA</Description>
            <Civilopedia>TXT_KEY_CIV5_TERRAIN_TUNDRA_TEXT</Civilopedia>
and so now the required "building" listed for Keldur Hall and the entry it directs to looks like the civilopedia entry on Tundra!
 
Back
Top Bottom