• We created a new subforum for the Civ7 reviews, please check them here!

[TOTPP] Prof. Garfield's Lua Code Thread

Yes, I think I may have. :think: Trying to jog my memory, but it's very possible.
Well, try this replacement file. You should get a message that the "general" rank has been removed when you load the game. Let me know if you still generate errors.
 

Attachments

Today I am trying to add shields to the nearest city to the unitKilled. I have city.totalShield and city.shields (in my understanding, the first is the production value, and the second is the # of shields currently provided towards whatever is being produced, correct?). In my attempt to increase those numbers with a function, the console says I am attempting arithmetic on a nil value. I've tried a few different ways. Some provide no error, but nothing happens, while others provide this error. I've tried making a frankenstein between the nearestCity part from your tutorial and the cityYield code, but to no avail. Any thoughts on this?
 
If you want add, say, 8 shields to a city's shield box, you use the code
Code:
cityObject.shields = cityObject.shields + 8
You do not need to use calculateCityYield for this, and you shouldn't do it there, for risk of adding shields to the production box every time someone opens the city screen.

You can't change city.totalShield, since that is just recording a number, so the game doesn't have to re-calculate all the cities when you look at the production adviser or something.

I don't know why you'd get an arithmetic on nil value error for these, unless you were trying to do that for a different object (maybe the tile under the city or something).

A production bonus each turn is trickier, but I think this will work:
This code can go in a convenient location, it doesn't really matter where.
Code:
local cityData = require("cityData")
-- A counter for the bonus shields that a city receives each turn.
--Default is 0, and minimum number is 0, no maximum number.
cityData.defineCounter("bonusShields",0,0)
-- A counter for the number of turns during which extra shields will be
-- received.  Default is 0, minimum is 0, no maximum.
-- This counter will be decremented manually each turn, during the 
-- onCityProduction event, and when it reaches 0, the bonusShields
-- counter will be reset to 0.
cityData.defineCounter("remainingTurnsForBonusShields",0,0)

-- bonus decrementing event.
discreteEvents.onCityProcessingComplete(function (turn, tribe)
    for city in civ.iterateCities() do
        if city.owner == tribe then
            if cityData.counterGetValue(city,"remainingTurnsForBonusShields") > 0 then
                cityData.counterSubtract(city,"remainingTurnsForBonusShields",1)
            end
            if cityData.get(city,"remainingTurnsForBonusShields") == 0 then
                -- you may wish to add a message here, to draw
                -- attention to the fact that the city's prodction
                -- has changed.
                cityData.counterReset(city,"bonusShields")
                cityData.counterReset(city,"remainingTurnsForBonusShields")
            end
        end
    end
end)
This line must go in MechanicsFiles\calculateCityYield.lua,
Code:
-- line for MechanicsFiles\calculateCityYield.lua
-- to be placed after the line
-- local shieldChangeAfterWaste = 0
shieldChangeAfterWaste = shieldChangeAfterWaste + cityData.counterGetValue(city,"bonusShields")
to be placed at some point after the line
Code:
local shieldChangeAfterWaste = 0
This function will bestow the bonus:
Code:
---Adds a bonus to the city's shield production for a number of turns.
---If the city already has a bonus, the new bonus replaces the old one.
---@param city cityObject
---@param bonus integer
---@param turns integer
local function bestowShieldBonus(city,bonus,turns)
    cityData.counterSetValue(city,"bonusShields",bonus)
    cityData.counterSetValue(city,"remainingTurnsForBonusShields",turns)
end
(You don't need to keep the description comment.)

Let me know if you need more help.
 
I see, thank you Prof., this is making sense in principle, but will take a bit to wrap my head around (the second part). I'll give it a shot regardless and report the results as it might help others.

So if I am dealing with the nearest city and simply adding to the shield box, what combination would I do?

Code:
function(nearestFriendlyCity)
bestCitySoFar.cityObject.shields = bestCitySoFar.cityObject.shields + 8

For example? Or would I create a new local function that does all of it together? (finds the city and adds production at the same time)

Or does it become
function(bestCitySoFar)
cityObject.shields = cityObject.shields + 8?

Trying to find a way to combine these, but also make sense of what order they would go in.
 
Here's some more code:

Code:
local function nearestFriendlyCity(tile,tribe)
    local bestCity = nil
    local bestDistance = 1000000
    for city in civ.iterateCities() do
        if city.owner == tribe then
            local distance = gen.distance(city.location,tile)
            if distance < bestDistance then
                bestCity = city
                bestDistance = distance
            end
        end
    end
    return bestCity
end

local function addShieldsToNearestCity(tile,tribe,shields)
    local city = nearestFriendlyCity(tile,tribe)
    if city then
        city.shields = city.shields + shields
        -- could also do something with bestowShieldBonus
    end
end
Then, you'd call something like
Code:
addShieldsToNearestCity(winner.location,winner.owner,8)
in your unitKilled event.

I hope this helps.
 
This is great Prof, thanks. The last bit worked well. For some reason it messed with my nearestFriendlyCity local function. It had an issue with "tileB." So I removed those two parts, and it works great even without tileB. It has found the nearest city and added shields without flaw, yet. That's curious, though.

For the first code you posted, the first three lines seem to need to be in the cityYield lua file near the top. Then it worked great! Before that, every time I opened a city screen, it gave me a console error. Anyway, this is game-changing! Looking forward to playing around with these possibilities. Will keep you posted.
 
Here is my latest. I tried this and a few iterations. How does the onEnterTile function work? I'm trying to enable this unit to have a chance of addFood when it enters certain terrain types.
No console error, just nothing happening. (I defined the addFoodToNearestCity earlier, and it works with other functions! So that's been cool.)

Code:
discreteEvents.onEnterTile(function (unit, previousTile, previousDomainSpec)
    if math.random() < 0.67 then
        if unit == unitAliases.Shepherd and unit.location.terrainType == 0 then do
            addFoodToNearestCity(unit.location,unit.owner,10)
            end
        end
    end
end)
 
This is great Prof, thanks. The last bit worked well. For some reason it messed with my nearestFriendlyCity local function. It had an issue with "tileB." So I removed those two parts, and it works great even without tileB. It has found the nearest city and added shields without flaw, yet. That's curious, though.
If you define 2 functions with the same name in the same file, only the last one will work. (Unless you use the first one outside of a function before the second one is defined.) I'd have to see the code to know what was wrong.

For the first code you posted, the first three lines seem to need to be in the cityYield lua file near the top.
The line
Code:
local cityData = require("cityData")
will have to be in every file that uses a cityData function. The defineCounter lines can't be duplicated in multiple files, but it doesn't matter where they are.

Here is my latest. I tried this and a few iterations. How does the onEnterTile function work? I'm trying to enable this unit to have a chance of addFood when it enters certain terrain types.
No console error, just nothing happening. (I defined the addFoodToNearestCity earlier, and it works with other functions! So that's been cool.)
Code:
unit == unitAliases.Shepherd
should be
Code:
unit.type == unitAliases.Shepherd
Don't feel bad. I make this kind of error more frequently than I'd like to admit.

FYI, you don't need to make a 'do' block here:
Code:
        if unit == unitAliases.Shepherd and unit.location.terrainType == 0 then do
            addFoodToNearestCity(unit.location,unit.owner,10)
            end
        end
This would work fine
Code:
        if unit.type == unitAliases.Shepherd and unit.location.terrainType == 0 then 
            addFoodToNearestCity(unit.location,unit.owner,10)
        end
 
Ooooof, Prof. This is like learning a language, except, with language, when you make mistakes you can still communicate. Thanks for pointing this out. I've found as we go, I tend to overcomplicate things! A good lesson.
 
Hi Prof., here is some code giving me issues.

In a saved game with 1 settler, when I load it, it functions fine... the pop-up comes up, and the unit generates.
When I start a new game though, and found that first city, it does not work, but it does for tribe ID 1 (I cheat and check). So this is quite odd! Any insights?

In case unclear, I want to, after the first city is founded, have a dialogue that lets you pick from 2 choices. But if AI, it randomly selects, and the dialog won't appear to the human player.

Spoiler :
Code:
local FirstChoice = civ.ui.loadImage("Images/firstchoice.bmp")
discreteEvents.onCityFounded(function(city)
        gen.justOnce("FirstChoice",function()
        local choice = nil
        if city.owner.isHuman then
        local dialog = civ.ui.createDialog()
                dialog.title = "Your first choice..."
                dialog.width = 500
                dialog:addImage(FirstChoice)
                local multiLineText = "You have unified several nomadic tribes in the vicinity, and now seek to settle down.\n^ \n^Your people have many talents. As you settle down, what would you like to focus on?"
                text.addMultiLineTextToDialog(multiLineText,dialog)
                    dialog:addOption("We will need more food in the coming years.", 1)
                    dialog:addOption("Ore and timber are of the utmost importance.", 2)
                 choice = dialog:show()
        else
                 choice = math.random(1,2)    
        end
            if choice == 1 then
                gen.createUnit(unitAliases.Shepherd,civ.getCurrentTribe(),{0,0},{count = 1, randomize = false, scatter = false, inCapital = true, veteran = true, homeCity = nil, overrideCanEnter = false, overrideDomain = false, overrideDefender = false})
            elseif choice == 2 then
                gen.createUnit(unitAliases.Ranger,civ.getCurrentTribe(),{0,0},{count = 1, randomize = false, scatter = false, inCapital = true, veteran = true, homeCity = nil, overrideCanEnter = false, overrideDomain = false, overrideDefender = false})
                
        end
    end)
end)
 
gen.justOnce("FirstChoice",function()
gen.justOnce makes sure that the enclosed function (2nd argument) is run only once per game. This is determined by the first key, in this instance, "FirstChoice". Once gen.justOnce has been executed for the "FirstChoice" key, the "FirstChoice" is stored in a table, and so gen.justOnce will never execute again when the first argument is "FirstChoice", even if the second argument is a different function in a different part of code. If you want this to be once per tribe, the way to do it is to have a different key per tribe, like this:

Code:
gen.justOnce("FirstChoice"..tostring(city.owner.id),function()

I hope this helps.
 
I'd like to verify that the scenario template is active in my ToTPP mod, but I'm having trouble accessing the Lua console. I'm running ToT in Wineskin on macOS. I've tried Ctrl+Shift+(Fn+)F3 and Cmd+Shift+(Fn+)F3 in cheat mode. Neither seems to have any effect

Is there another way to access it, such as through a menu?

I'm on ToTPP 0.18.4
 
I'd like to verify that the scenario template is active in my ToTPP mod, but I'm having trouble accessing the Lua console. I'm running ToT in Wineskin on macOS. I've tried Ctrl+Shift+(Fn+)F3 and Cmd+Shift+(Fn+)F3 in cheat mode. Neither seems to have any effect

Is there another way to access it, such as through a menu?

I'm on ToTPP 0.18.4

Do you have the Lua Scripting patch enabled? I think that it is not enabled by default. If you do, then I don't know what the problem is.

totpplauncher.png
 
Thanks Prof. What is frustrating is that I've used that before, and for some reason did not register that it could be an issue. Thank you!

Quick question: How do I draw rivers into a function, if they are not a terrain object?
E.g., if I wanted to register river base terrain and all adjacent base terrains to rivers (excluding ocean), and then morph those terrain for, say, X# of turns, and then morph them back to their original terrain. (but keeping the rivers on top of their own terrain)
How does the system code rivers, and how would it revert them back?

I'm thinking a similar function to you accomplished for the goodie huts... the game registers where the rivers are, and then, on a random turn, it turns the base terrain under the rivers and all adjacent terrain into another terrain, and reverts them back somehow).

I'm not sure what rivers are IDed as in the lua language.
 
How does the system code rivers, and how would it revert them back?
You can use tile.river to place/remove rivers. My Lua lessons were written before TNO gave us that functionality.

Quick question: How do I draw rivers into a function, if they are not a terrain object?
E.g., if I wanted to register river base terrain and all adjacent base terrains to rivers (excluding ocean), and then morph those terrain for, say, X# of turns, and then morph them back to their original terrain. (but keeping the rivers on top of their own terrain)
How does the system code rivers, and how would it revert them back?
I'm not entirely sure what you want to do, but perhaps these functions will help you:
Code:
local tileData = require("tileData")
tileData.defineCounter("originalBaseTerrainID",-1)

local function riverNearby(tile)
    if tile.river then
        return true
    end
    for _,adjacentTile in pairs(gen.getAdjacentTiles(tile)) do
        if adjacentTile.river then
            return true
        end
    end
    return false
end

local newTerrain = civ.getBaseTerrain(0,0)
local function changeRiverNeighbours()
    for tile in civ.iterateTiles() do
        if riverNearby then
            tileData.counterSetValue(tile,"originalBaseTerrainType",tile.baseTerrain.type)
            tile.baseTerrain = newTerrain 
        end
    end
end

local function restoreRiverTerrain()
    for tile in civ.iterateTiles() do
        if not tileData.counterIsNil(tile,"originalBaseTerrainID") then
            local baseTerrainType = tileData.counterGetValue(tile,"originalBaseTerrainID")
            tile.baseTerrain = civ.getBaseTerrain(tile.z,baseTerrainType)
            tileData.counterReset(tile,"originalBaseTerrainID")
        end
    end
end
Note that depending on how large your map is, iterating over every tile can sometimes cause a bit of lag. This isn't really a problem if you're doing something once per turn.

I hope this helps.
 
Hi Prof.! Excited to try it out. Thank you for that. Will let you know how it goes.

Here is something strange. This code I wrote was working really well earlier today, and now it isn't. No errors, just nothing happening. I tried parsing them into their own discreteEvent, but that also did not work.

Spoiler :
Code:
discreteEvents.onEnterTile(function (unit, previousTile, previousDomainSpec)
    if math.random() < 0.25 then
        if unit.type == unitAliases.Shepherd and unit.location.terrainType == 1 then
            addFoodToNearestCity(unit.location,unit.owner,shepherdFoodTable[math.random()])
            if unit.owner.isHuman then
            civ.playSound("Food.wav")
            end
        end
    end
    if math.random() < 0.25 then
        if unit.type == unitAliases.Shepherd and unit.location.terrainType == 0 then
            addFoodToNearestCity(unit.location,unit.owner,shepherdFoodTable[math.random()])
            if unit.owner.isHuman then
            civ.playSound("Food.wav")
            end
        end
    end
    if math.random() < 0.25 then
        if unit.type == unitAliases.Ranger and unit.location.terrainType == 3 then
            addShieldsToNearestCity(unit.location,unit.owner,rangerShieldsTable[math.random()])
            if unit.owner.isHuman then
            civ.playSound("Production.wav")
            end
        end
    end
    if math.random() < 0.25 then
        if unit.type == unitAliases.Ranger and unit.location.terrainType == 4 then
            addShieldsToNearestCity(unit.location,unit.owner,rangerShieldsTable[math.random()])
            if unit.owner.isHuman then
            civ.playSound("Production.wav")
            end
        end
    end
    if math.random() < 0.25 then
        if unit.type == unitAliases.Statesman1 and unit.location.improvements then
            unit.owner.money = unit.owner.money + titheTable[math.random()]
        end
    end
end)
 
Here is something strange. This code I wrote was working really well earlier today, and now it isn't. No errors, just nothing happening. I tried parsing them into their own discreteEvent, but that also did not work.
Have you called the function civ.scen.onActivateUnit anywhere? That function is only supposed to be called once, and onEnterTile works by using the call I defined in events.lua (and doing some other stuff I won't get into). If you called civ.scen.onActivateUnit, that would erase the other function registered to it.

If you haven't done this, I'd try putting in some print statements to check if the code is being called. You may simply have been a bit unlucky.
 
Have you called the function civ.scen.onActivateUnit anywhere? That function is only supposed to be called once, and onEnterTile works by using the call I defined in events.lua (and doing some other stuff I won't get into). If you called civ.scen.onActivateUnit, that would erase the other function registered to it.

If you haven't done this, I'd try putting in some print statements to check if the code is being called. You may simply have been a bit unlucky.
That did it! And then where I had the civ.scen.onActivateUnit, I replaced with a discreteEvent, which covered my tracks. Now all is well. Thank you!
 
Top Bottom