[TOTPP] Prof. Garfield's Lua Code Thread

(From a PM)
So the error message you coded in, "No volcano found" is indeed coming up, yet when I "reveal map," there are 2 volcanoes as there should be. I know I can just remove the error message, but just curious why this is happening.
Would you suggest changing all of my "getRandomX" code to this style?
The for loop style code tries to find an appropriate random tile 1000 times (~500 if you count times where the numbers generated were not valid coordinates), and, if it can't then generates an error. (You could also just set some default behaviour.) If there are only 2 tiles on the map, then randomly checking tiles isn't very likely to find a volcano.

Instead, you should probably loop over all the tiles, save the volcano squares, and choose randomly between them when appropriate. You'd keep a table similar to the 'hut table' that we discussed earlier. Or, if you are generating the volcanoes via code, just record where they were created then.

It couldn't hurt to change the code you have, but it probably isn't necessary either. I would write new code this way (perhaps with a text box and default behaviour rather than outright error) so that failure isn't in the form of an infinite loop.
 
Thanks Prof.

The issue now is that the volcanoes are placed, but the game can't find them. I'm wondering if there's a way to save where they were created -- else a different way to place them entirely -- so that the game can find specific tiles.
 
Thanks Prof.

The issue now is that the volcanoes are placed, but the game can't find them. I'm wondering if there's a way to save where they were created -- else a different way to place them entirely -- so that the game can find specific tiles.
Here's some code to keep track of volcanoes and choose a random one when appropriate:
Code:
local function placeVolcano(tile)
    tile.baseTerrain = civ.getBaseTerrain(0,15)
    local state = gen.getState()
    if not state.volcanoTiles then
        state.volcanoTiles = {}
        for tile in civlua.iterateTiles() do
            if tile.baseTerrain.type == 15 then
                state.volcanoTiles[1+#state.volcanoTiles] = gen.getTileID(tile)
            end
        end
    end
    state.volcanoTiles[1+#state.volcanoTiles] = gen.getTileID(tile)
end

local function removeVolcano(tile,newBaseTerrain)
    if newBaseTerrain.type == 15 then
        error("Can't remove volcano with a volcano")
    end
    tile.baseTerrain = newBaseTerrain
    local state = gen.getState()
    if not state.volcanoTiles then
        state.volcanoTiles = {}
        for tile in civlua.iterateTiles() do
            if tile.baseTerrain.type == 15 then
                state.volcanoTiles[1+#state.volcanoTiles] = gen.getTileID(tile)
            end
        end
    end
    local tileID = gen.getTileID(tile)
    for i = 1, #state.volcanoTiles do
        if state.volcanoTiles[i] == tileID then
            state.volcanoTiles[i] = nil
        end
    end
    gen.makeArrayOneToN(state.volcanoTiles)
end

local function getRandomVolcano()
    local state = gen.getState()
    if not state.volcanoTiles then
        state.volcanoTiles = {}
        for tile in civlua.iterateTiles() do
            if tile.baseTerrain.type == 15 then
                state.volcanoTiles[1+#state.volcanoTiles] = gen.getTileID(tile)
            end
        end
    end
    local numberOfVolcanoes = #state.volcanoTiles
    if numberOfVolcanoes == 0 then
        -- you could change this to return nil or
        -- handle the situation some other way
        error("No volcanoes found")
    end
    local choiceIndex = math.random(1,numberOfVolcanoes)
    local tileID = state.volcanoTiles[choiceIndex]
    local volcanoTile = gen.getTileFromID(tileID)
    if volcanoTile.baseTerrain.type ~= 15 then
        -- We shouldn't get here, but just in case
        -- a volcano was removed without updating the state table
        state.volcanoTiles[choiceIndex] = nil
        gen.makeArrayOneToN(state.volcanoTiles)
        -- Try again
        -- since the number of volcanoes has decreased, this should eventually terminate
        return getRandomVolcano()
    end
    return volcanoTile
end
 
OK! Time for some fun stuff in celebration of this. A good spot for a volcano: increased fertility! But... it is Pompeii...

Spoiler :
volcano1.png


volcano2.png


volcano3.png


In the end, this eruption went the other direction. Not a terrible outcome for Pompeii, but not ideal, either.
 
I'm getting tired of fortress spam, and want them to have a bit more "oomph." I know the AI will totally flub this, so I also wanted to keep a random element for the human.

Prof., I used your athletic arena code, and adapted it:

Spoiler :
Code:
discreteEvents.onCityProcessingComplete(function (turn, tribe)
    if tribe:hasTech(techAliases.Construction) then
        local fortCity = nil
                    if tribe.isHuman then
                        local menuTable = {}
                        local offset = 3
                        for city in civ.iterateCities() do
                            if city.owner == civ.getCurrentTribe() then
                                menuTable[offset+city.id] = city.name
                            end
                        end
                        local fortChoice = text.menu(menuTable,"We have the materials and expertise for a new fortress. Select a city.","A New Fortress")
                        if fortChoice >= offset then
                            fortCity = civ.getCity(fortChoice-offset)
                        end
                else
                    local aiChoiceList = {}
                    local index = 1
                    for city in civ.iterateCities() do
                        if city.owner == tribe then
                            aiChoiceList[index] = city.id
                            index = index + 1
                        end
                    end
                    if index > 1 then
                    local aiChoice = aiChoiceList[math.random(1,index-1)]
                    fortCity = civ.getCity(aiChoice)
                    end
                end
                local fortCityRadius = randomCityRadiusTile(fortCity)
                if civ.isCity(fortCity) then
                    gen.placeFortress(fortCityRadius)
                end
    end
end)

And it works! But I was looking for advice for tinkering with the end of this.

Specifically, I want the "gen.placeFortress(fortCityRadius)" to prefer highly defensible tile types, e.g., hills, forests, desert, over less defensible tiles, but still place the fortress on "lesser" tiles if it can't find the preferred ones.

I'd also like to prevent the fortress from being placed on the sea. (although, how cool is it that this is an option...)

This does mean less player agency on where to place forts (chokeholds, etc.), but if everyone is in the same situation, I think it's rather fair. Fortresses will have some other neat features, too.

Any advice would be much appreciated, as always!
 
Spoke too soon! This is acting strangely now.

I'm curious about this function, and how to draw up the table. How do I access the table of radius tiles, with those numbers?

I was thinking I'd do a series of "if/else" code, prioritizing hills and going on down the terrain chain, but not sure if this would work.
 
Spoke too soon! This is acting strangely now.

I'm curious about this function, and how to draw up the table. How do I access the table of radius tiles, with those numbers?

I was thinking I'd do a series of "if/else" code, prioritizing hills and going on down the terrain chain, but not sure if this would work.
In most circumstances, you would probably want to loop over the keys and values using pairs. There are some examples of that in the code that follows.
Specifically, I want the "gen.placeFortress(fortCityRadius)" to prefer highly defensible tile types, e.g., hills, forests, desert, over less defensible tiles, but still place the fortress on "lesser" tiles if it can't find the preferred ones.

I'd also like to prevent the fortress from being placed on the sea. (although, how cool is it that this is an option...)

This does mean less player agency on where to place forts (chokeholds, etc.), but if everyone is in the same situation, I think it's rather fair. Fortresses will have some other neat features, too.

Any advice would be much appreciated, as always!
What you probably want do do is to assign 'weights' to each tile, and choose a tile based on those weights. The General Library has some functions to do this.

Example 1. Define a 'weight function' which assigns a weight to each tile (or false, if the tile should never be chosen). Then, choose randomly between the tiles, giving tiles with higher weight a greater chance of being chosen.
Code:
-- returns the weight (value) of the tile, or 
-- false if the tile can't be chosen
local function weighTile(tile,nearbyCity)
    local weight = 0
    if tile.city or gen.hasFortress(tile) or tile.baseTerrain.type == 10
    or (tile.defender and tile.defender ~= nearbyCity.owner) then
        return false
    end
    weight = weight + tile.baseTerrain.defense
    if tile.river then
        weight = weight + 1
    end
    if gen.hasTransportation(tile) then
        weight = weight + 3
    end
    if hasFortifiedUnit(tile,nearbyCity) then
        weight = weight + 5
    end
    return weight
end

-- chooses a random tile on which to build a fortress,
-- with probabilities based on tile weight
local function chooseFortressTileNearCity2(city)
    local tileList = gen.cityRadiusTiles(city)
    local eligibleTiles, tileWeights = gen.getBiggestWeights(tileList,weighTile,#tileList,city)
    local totalWeight = 0
    local choiceThresholdTable = gen.makeThresholdTable()
    for i=1,#eligibleTiles do
        choiceThresholdTable[totalWeight] = eligibleTiles[i]
        totalWeight = totalWeight + tileWeights[i]
    end
    return choiceThresholdTable[totalWeight*math.random()]
end
The weight function returns false for tiles with cities, fortresses, foreign units, or ocean squares. Otherwise, the weight is 3 points for having a road/railroad, 5 points for having a fortified unit on the tile, and 1 point for each 50% defense bonus the tile would give.

First, gen.cityRadiusTiles gets a table of eligible tiles. Then gen.getBiggestWeights sorts them and discards ineligible tiles. All the weights are added up to get a totalWeight. During that process, items are added to a threshold table, with the threshold being the current sum of weights. After that, a random number is chosen between 0 and the final value of totalWeight. This effectively makes a choice where the chance of each tile is proportional to its weight (as long as weights are positive). If it's not clear why, try a couple small examples on paper, and ask if it is still unclear after that.

Example 2: Define a 'weightTable' to compute the weights, get the 3 highest weight tiles, and choose one of those 3 with equal probability.
Code:
local function hasCity(tile)
    return tile.city ~= nil
end
local function hasFortifiedUnit(tile,city)
    for unit in tile.units do
        if unit.owner == city.owner and gen.isFortified(unit) then
            return true
        end
    end
    return false
end

local function hasForeignUnit(tile,city)
    if tile.defender and tile.defender ~= city.owner then
        return true
    end
end

local fortressWeightTable = {
    -- If the tile has a road (or railroad), add 3 points to the weight
    [gen.hasTransportation] = 3,
    -- If the tile already has a fortress, don't let another one be built,
    [gen.hasFortress] = false,
    -- If the tile has a city, don't let a fortress be built
    [hasCity] = false,
    -- Add a point for each 50% increase in defense for the tile
    [function() return true end] = function(tile)
        local defense = tile.baseTerrain.defense
        if tile.river then
            defense = defense + 1
        end
        return defense
    end,
    -- Return false if the tile is ocean
    [function(tile) return tile.baseTerrain.type == 10 end] = false,
    -- If the tile is adjacent to the city, it gets 2 extra points
    [function(tile,city) return gen.distance(tile,city) == 1 end] = 2,
    -- If the tile has a fortified unit, it gets 5 extra points
    [hasFortifiedUnit] = 5,
    -- If the tile has a foreign unit, it don't build the fortress
    [hasForeignUnit] = false,
}

-- returns a tile to build a fortress on
-- chosen at random from best 3 tiles
local function chooseFortressTileNearCity(city)
    local tileList = gen.cityRadiusTiles(city)
    local shortListNumber = 3 -- choose from the best 3 tiles
    local shortList, shortListWeights = gen.getBiggestWeights(tileList,fortressWeightTable,shortListNumber,city)
    return shortList[math.random(1,shortListNumber)]
end
A weightTable is defined in the documentation for gen.calculateWeight . Basically, if the function which is the table key returns true, add the value to the computed weight for that item. If the weight is false, then exclude the tile if the key is true. If the weight is true, the key function must return true or the tile (or, more generally, item) can't be chosen.

Note: normally we consider tables with integers or strings as keys. However, other values can be keys too, so in this example, functions are used. Be careful with this, however, since sometimes thing which look the same to us would be considered different keys. For example,
Code:
local myTable = {}
myTable[civ.getUnitType(0)] = "Unit Type 0"
print(myTable[civ.getUnitType(0)]) --> nil
But there is still a unitTypeObject,string pair in the table that you could access with pairs.

After running through gen.getBiggestWeights, the shortList of 3 tiles is generated, and one is chosen at random. With the code the way it is, if there are fewer than 3 options, there is a chance nil is chosen even if there is a valid option.

Here are the key press events that I tested this code with:
Code:
local mostRecentTile = nil
discreteEvents.onKeyPress(function(keyID)
    if keyID == keyboard.seven then
        if mostRecentTile then
            gen.removeFortress(mostRecentTile)
            gen.chartTruthfully(mostRecentTile,civ.getCurrentTribe())
            civ.ui.centerView(mostRecentTile)
            mostRecentTile = nil
        end
        return
    end
    if keyID == keyboard.nine and civ.getCurrentTile().city then
        local city = civ.getCurrentTile().city
        local tile = chooseFortressTileNearCity(city)
        if tile then
            gen.placeFortress(tile)
            mostRecentTile = tile
            gen.chartTruthfully(mostRecentTile,civ.getCurrentTribe())
            civ.ui.centerView(mostRecentTile)
        else
            civ.ui.text("No tile found")
        end
    end
    if keyID == keyboard.eight and civ.getCurrentTile().city then
        local city = civ.getCurrentTile().city
        local tile = chooseFortressTileNearCity2(city)
        if tile then
            gen.placeFortress(tile)
            mostRecentTile = tile
            gen.chartTruthfully(mostRecentTile,civ.getCurrentTribe())
            civ.ui.centerView(mostRecentTile)
        else
            civ.ui.text("No tile found")
        end
    end
end)
Pressing 7 clears the last fortress, 8 chooses any valid tile, and 9 gives equal weight to the three best tiles. The cursor must be on a city for this to work.
 
This opens up so much. I feel like this is the next lesson in an overall education on this stuff. I'll tinker and explore, and ask along the way if things come up. I can see so much utility, including on movement and how the AI might move/think. Thanks for the tutorial, Prof.!

My solution to the fortress selection was to create a dialogue, and give tiles 1-20 a selection on that dialogue, with an image of an example map next to it. This way, the player can select the tile they want to build a fortress on from a menu. The menu is huge, as I haven't figured out yet how to make 2 columns in a select dialogue.

If a player accidentally selects an ocean tile (I need to write in this dialog that they cannot), it says the construction effort was swept away by floodwaters, and a materials unit is placed in the capital. (Below was the first test; later added "4" which is missing here.)

Spoiler :
fortlayout.png


The only issue I foresee is this menu coming up, and a player not able to see their city to really select a good spot. I'm wanting to make a button, "ask me next turn," or something similar, or maybe the menu comes up again at the end of the turn.

Another smaller issue in all of my code is that I am unable to figure out how to call on a city's name within a dialog (e.g., "drought strikes X"). Is there a tutorial on dialogs somewhere?
 
My solution to the fortress selection was to create a dialogue, and give tiles 1-20 a selection on that dialogue, with an image of an example map next to it. This way, the player can select the tile they want to build a fortress on from a menu. The menu is huge, as I haven't figured out yet how to make 2 columns in a select dialogue.
As far as I know that is not possible. text.menu will automatically split a menu into multiple "pages" if there are too many options.

Another smaller issue in all of my code is that I am unable to figure out how to call on a city's name within a dialog (e.g., "drought strikes X"). Is there a tutorial on dialogs somewhere?
The command cityObject.name get's you a city's name from its cityObject. If that's not the answer, then I don't understand the question.
 
That was exactly it. Thank you, Prof.

I was wondering if you have ever had any experience with this, or know a place/thread I might find some answers.

For a few events in my lua code, I have "invisible" tech given to the tribe. When this happens, the tech is viewable if you go to the Science Advisor. This is fine, and is a good way to see that the event is happening.

For one of these events, wherein the tech in question is a prerequisite for a special unit, the event proceeds as it should, and the unit becomes buildable.

However, for another event, wherein I want the tech in question to be a prerequisite for a wonder, while the tech is viewable in Science Advisor, the wonder does not become buildable. My rules.txt prerequisites are matching, so I'm wondering why this wonder isn't showing up. Is there a way to "force" the game to read: if a tribe has:tech, it can build wonder?

Tried this, but it didn't seem to work.

Code:
if civ.isWonder(item) then
     if item == wonderAliases.AcademyofGondishapur then
          local tribe = civ.getCurrentTribe()
          return tribe:hasTech(techAliases.Acceptance) and defaultBuildFunction(city,item)
     end
end
 
You should definitely use
Code:
local tribe = city.owner
I don't think that's the problem, but I don't know if the game ever checks for canBuild during other players turns, and there is no reason to risk it.

First, did you change the code in events.lua to remove the civ.scen.onCanBuild call that I put in? If not, then the game is probably still constructing code using information registered in canBuildSettings.lua rather than your alternate code for this purpose.

Second, are you absolutely sure that you haven't misspelled
Code:
wonderAliases.AcademyofGondishapur

Third, have you tried returning just
Code:
return tribe:hasTech(techAliases.Acceptance)

If this works, then there is something weird with how the game chooses when you can build the wonder. If not, the game either isn't detecting that the tribe has the relevant tech, or there is something wrong with your code somewhere.
 
Returning only the techAlias did the trick. Thanks Prof.!

First, did you change the code in events.lua to remove the civ.scen.onCanBuild call that I put in? If not, then the game is probably still constructing code using information registered in canBuildSettings.lua rather than your alternate code for this purpose.

I am noticing that all of my code for requiring certain improvements to build units (for the human player) is no longer working. Is this likely why?
I tried copying/pasting all of that code to canBuildSettings.lua, in addition to the alias tables, but could not get it to work. Any thoughts why this might have stopped functioning?
 
Returning only the techAlias did the trick. Thanks Prof.!
This might mean that the wonder will show up in the menu after it is finished. In any case, it means that there was some reason why the game wouldn't let you build the wonder even with normal rules.
I am noticing that all of my code for requiring certain improvements to build units (for the human player) is no longer working. Is this likely why?
I tried copying/pasting all of that code to canBuildSettings.lua, in addition to the alias tables, but could not get it to work. Any thoughts why this might have stopped functioning?
If you want an arbitrary function to determine whether something can or can't be built while still using canBuildSettings.lua, you need to add a conditionFunction or overrideFunction for the item in question
Code:
--      .conditionFunction = function(defaultBuildFunction,city,item) --> bool
--          if function returns true, item can be built if other conditions are met, if false, item can't be built
--          absent means no extra condition
--      .returnFalse = bool or nil
--          if true, item can't be built
--          if false or nil, refer to other conditions
--          (happens before overrideFunction and alternateParameters)
--      .overrideFunction = function(defaultBuildFunction,city,item) --> boolean
--          if function returns true, the city is automatically allowed to build the item, regardless of any
--          conditions that isn't met
--          if function returns false, the other conditions are checked
If you're registering your own function using civ.scen.onCanBuild, this means you're not using the one generated by canBuild.lua.
 
So for this code, for example -- I have a huge list of conditions. What is the easiest way to have these work again? Does this mean adding the condition to each item?
Or if I were to copy/paste these into another file, is it canBuild or canBuildSettings, and where would I paste them in those files? Sorry for the million questions! A bit lost as I navigate splitting into different files.
Spoiler :
Code:
local function inBuildMenu(defaultBuildFunction,city,item,tribe)
-- Really useful for controlling unit population
if civ.isUnitType(item) then
    if item == unitAliases.Garrison then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Garrison,city.owner) <= findNativePopulation(city.owner)/6
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.Archers then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Archers,city.owner) <= findNativePopulation(city.owner)/4
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.Marines then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Marines,city.owner) <= findNativePopulation(city.owner)/4
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.Legion then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Legion,city.owner) <= findNativePopulation(city.owner)/8
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.Cataphract then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Cataphract,city.owner) <= findNativePopulation(city.owner)/8
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.HeavyCavalry then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.HeavyCavalry,city.owner) <= findNativePopulation(city.owner)/6
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.LightCavalry then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.LightCavalry,city.owner) <= findNativePopulation(city.owner)/4
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.Ballista then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Ballista,city.owner) <= findNativePopulation(city.owner)/8
        end
    end
end
-- Begin counting for special units obtained through Tributaries tech
if civ.isUnitType(item) then
    if item == unitAliases.Barque then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Barque,city.owner) <= findNativePopulation(city.owner)/6
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.Elephants then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Elephants,city.owner) <= findNativePopulation(city.owner)/6
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.Messenger then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Messenger,city.owner) <= findNativePopulation(city.owner)/6
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.Artisans then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Artisans,city.owner) <= findNativePopulation(city.owner)/4
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.HillWarriors then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.HillWarriors,city.owner) <= findNativePopulation(city.owner)/6
        end
    end
end
-- End counting for special units obtained through Tributaries tech
if civ.isUnitType(item) then
    if item == unitAliases.BatteringRam then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.BatteringRam,city.owner) <= findNativePopulation(city.owner)/3
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.SiegeHook then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.SiegeHook,city.owner) <= findNativePopulation(city.owner)/6
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.Catapult then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Catapult,city.owner) <= findNativePopulation(city.owner)/6
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.Galley then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Galley,city.owner) <= findNativePopulation(city.owner)/4
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.Trireme then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Trireme,city.owner) <= findNativePopulation(city.owner)/4
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.Quadrireme then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Quadrireme,city.owner) <= findNativePopulation(city.owner)/6
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.Juggernaut then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Juggernaut,city.owner) <= findNativePopulation(city.owner)/8
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.SiegeShip then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.SiegeShip,city.owner) <= findNativePopulation(city.owner)/8
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.Liburna then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Liburna,city.owner) <= findNativePopulation(city.owner)/6
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.Shepherd then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Shepherd,city.owner) <= findNativePopulation(city.owner)/6
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.Ranger then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Ranger,city.owner) <= findNativePopulation(city.owner)/6
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.LightInf then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.LightInf,city.owner) <= findNativePopulation(city.owner)/3
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.HeavyInf then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.HeavyInf,city.owner) <= findNativePopulation(city.owner)/4
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.Cohort then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Cohort,city.owner) <= findNativePopulation(city.owner)/6
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.Hoplites then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Hoplites,city.owner) <= findNativePopulation(city.owner)/4
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.WarElephant then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.WarElephant,city.owner) <= findNativePopulation(city.owner)/8
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.Slingers then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.Slingers,city.owner) <= findNativePopulation(city.owner)/4
        end
    end
end
if civ.isUnitType(item) then
    if item == unitAliases.HorseArchers then
        if not defaultBuildFunction(city,item) then
            return false
        else
            return countUnits(unitAliases.HorseArchers,city.owner) <= findNativePopulation(city.owner)/4
        end
    end
end
if civ.isWonder(item) then
    if item == wonderAliases.AcademyofGondishapur then
        local tribe = city.owner
        return tribe:hasTech(techAliases.Acceptance)
    end
end
if civ.isWonder(item) then
    if item == wonderAliases.CouncilofNicaea then
        local tribe = city.owner
        return tribe:hasTech(techAliases.NewReligion)
    end
end
if city.owner.isHuman then
    if civ.isUnitType(item) then
        if item == unitAliases.Merchant or item == unitAliases.Caravan then
            return civ.hasImprovement(city,improvementAliases.Marketplace) and defaultBuildFunction(city,item)
        end
    end
    if civ.isUnitType(item) then
        if item == unitAliases.Galley or item == unitAliases.Bireme then
            return civ.hasImprovement(city,improvementAliases.Harbor) and defaultBuildFunction(city,item)
        end
    end
    if civ.isUnitType(item) then
        if item == unitAliases.Trireme or item == unitAliases.Quadrireme then
            return civ.hasImprovement(city,improvementAliases.Harbor) and defaultBuildFunction(city,item)
        end
    end
    if civ.isUnitType(item) then
        if item == unitAliases.Ballista or item == unitAliases.Cataphract then
            return civ.hasImprovement(city,improvementAliases.LayeredCrucibles) and defaultBuildFunction(city,item)
        end
    end
    if civ.isUnitType(item) then
        if item == unitAliases.Legion or item == unitAliases.Cohort then
            return civ.hasImprovement(city,improvementAliases.LayeredCrucibles) and defaultBuildFunction(city,item)
        end
    end
    if civ.isUnitType(item) then
        if item == unitAliases.Juggernaut or item == unitAliases.SiegeShip then
            return civ.hasImprovement(city,improvementAliases.Shipyard) and defaultBuildFunction(city,item)
        end
    end
    if civ.isUnitType(item) then
        if item == unitAliases.LightInf or item == unitAliases.HeavyInf then
            return civ.hasImprovement(city,improvementAliases.Barracks) and defaultBuildFunction(city,item)
        end
    end
    if civ.isUnitType(item) then
        if item == unitAliases.LightCavalry or item == unitAliases.HeavyCavalry then
            return civ.hasImprovement(city,improvementAliases.Barracks) and defaultBuildFunction(city,item)
        end
    end
    if civ.isUnitType(item) then
        if item == unitAliases.Marines then
            return civ.hasImprovement(city,improvementAliases.Harbor) and defaultBuildFunction(city,item)
        end
    end
end
    return defaultBuildFunction(city,item)
end
civ.scen.onCanBuild(inBuildMenu)
 
So for this code, for example -- I have a huge list of conditions. What is the easiest way to have these work again? Does this mean adding the condition to each item?
Or if I were to copy/paste these into another file, is it canBuild or canBuildSettings, and where would I paste them in those files? Sorry for the million questions! A bit lost as I navigate splitting into different files.
I suggest using the code generator script, found in the scripts folder. Have a look at this video for how to use it. Since nearly all of your restrictions are based on total population, I'll draw your attention to the setting:

"For each population point of the tribe's cities, increase the tribe ownership limit by this amount: "

So, if you want to divide the population by 4, you just use 0.25. Divide the population by 8, you use 0.125.
 
Prof. Garfield said:
This might mean that the wonder will show up in the menu after it is finished. In any case, it means that there was some reason why the game wouldn't let you build the wonder even with normal rules.

This is indeed happening. It still warns the player that it has been built. I wonder if the AI will keep trying to build it though?

Prof. Garfield said:
I suggest using the code generator script, found in the scripts folder. Have a look at this video for how to use it. Since nearly all of your restrictions are based on total population, I'll draw your attention to the setting:

Actually, the population restricted units are working perfectly in the above code. It is the improvement prerequisite code that is not working. What's strange is that it was working. So I'm trying to discover the reason why that part of my code might be broken.
 
Today I am looking at roads, and wondering about ideas on how to implement some features related to roads.
The first one is, is it possible to "register" two cities connected by road? Would this create a lot of lag?

I'm wondering if there is a way to prevent airlifts unless the cities are connected by a road.
And also, I'm wondering if there would be a way to "place road" on all tiles between 2 cities in a logical fashion. It seems impossible as I type it out, but wondering if Prof. or any others might have ideas on how to make this happen.
 
Today I am looking at roads, and wondering about ideas on how to implement some features related to roads.
The first one is, is it possible to "register" two cities connected by road? Would this create a lot of lag?
The module aStarCiv has the function aStarCiv.aStar, which does pathfinding
Code:
-- start is a tile object or table of such objects (if multiple places can act as the 'start')
-- goal is a tile object or table of such objects (if any one of them is a valid goal)
-- heuristic(startTile,endTileOrTable) --> number
--      an estimate of the cost from startTile to endTile
--      if the goal is specified as a table of tiles, heuristic should be able to
--      accommodate that and accept a table of tiles as an argument
--      does NOT have to accommodate a table for startTile, even if multiple
--      starting locations are valid
--  neighbours(tile,start,goal)--> table of {neighbouringTile,cost from tile to neighbouring tile}
--      returns the neighbouring tiles of a tile, and the cost to get to them
--      start and goal are included parameters (in case, for example, you want to limit how far away the path can take
--  pathCost(orderedTableOfTiles)-->number
--  stopLookingHeuristicCost
--      If the best estimated cost (fScore) exceeds this cost, stop looking and return false
--      default is math.huge (explore all possibilities)
Here is an example for checking if 2 cities are connected by road:
Code:
-- the distance between the current tile and the end will
-- always be less than or equal to the distance of the shortest
-- road between them
local function heuristicForConnectedRoad(currentTile,endTile)
    return gen.tileDist(currentTile,endTile)
end

-- choose adjacent tiles which have either road/rail or a city
-- ignore all other tiles
local function neighboursForConnectedRoad(tile,start,goal)
    local nIndex = 1
    local neighbours = {}
    local adjacentTiles = gen.getAdjacentTiles(tile)
    for _, adjacentTile in pairs(adjacentTiles) do
        if gen.hasTransportation(adjacentTile) or adjacentTile.city then
            neighbours[nIndex] = {adjacentTile,1}
            nIndex = nIndex+1
        end
    end
    return neighbours
end

-- the cost of a path is the number of tiles in it
local function pathCostForConnectedRoad(path)
    return #path-1
end

-- returns true if the cities are connected by road
-- and false otherwise
local function citiesConnected(city1,city2)
    if city1 == city2 then
        return true
    end
    if city1.location.landmass ~= city2.location.landmass then
        return false
    end
    local cost,path = aStarCiv.aStar(city1.location,city2.location,heuristicForConnectedRoad,neighboursForConnectedRoad,pathCostForConnectedRoad)
    return cost ~= false
end

discreteEvents.onKeyPress(function(keyid)
    if keyid == keyboard.nine then
        if not civ.getCurrentTile().city then
            return
        end
        civ.ui.text(tostring(citiesConnected(civ.getCity(0),civ.getCurrentTile().city)))
    end
end)

Trying to do this for a lot of cities at once could cause lag, especially if there are a lot of cities on the same landmass that aren't connected by roads. You'd have to just try it to know for sure.

I'm wondering if there is a way to prevent airlifts unless the cities are connected by a road.
The only way I know how would be to use the functions gen.isUsedAirport, gen.setUsedAirport, and gen.clearUsedAirport to change the cities that could be airlifted to or from based on the location of the active unit. I don't know if it is possible to make the changes quickly enough to take effect. You would probably want to save a table of cities connected to each other at the start of the player's turn, rather than continually recalculate.

In Over The Reich, I wrote a "trainlift" event which is similar to what you want (though it does nothing for the AI). Search events.lua for TRAINLIFT for the main section of code. I'm not sure how useful this will be, since I didn't write many comments.

And also, I'm wondering if there would be a way to "place road" on all tiles between 2 cities in a logical fashion. It seems impossible as I type it out, but wondering if Prof. or any others might have ideas on how to make this happen.
Use aStarCiv.aStar to find a "low cost" path between the cities. At the very least, you'll need to change the neighbours function compared to the earlier example, to account for this different kind of task.
 
Amazing possibilities, thanks for the insights Prof.! I'll tinker and report back as usual. :)

I had one other question today. I'm attempting something relatively straightforward, but it isn't working. I want, when a city builds X improvement, to place Y unit in any nearby fortress(es).

Tried this and a few variations, but couldn't get it to work out. I was able to load the event, though, backwards -- e.g., if the city builds the improvement first, and then later creates a fort, the unit generates. But not the other way around (fort already exists, improvement is made).

Code:
discreteEvents.onCityProduction(function (city,item)
if item == improvementAliases.FortressNetwork then
local tile = isInCityRadius()
    if gen.hasFortress(tile) then
         gen.createUnit(etc[placeholder])
     end
end
end)
 
Looks like you want to replace
Code:
local tile = isInCityRadius()
with a loop
Code:
for _,tile in pairs(gen.cityRadiusTiles(city) do
    if gen.hasFortress(tile) then
         gen.createUnit(etc[placeholder])
    end
end
To explain your reverse behaviour, I'd have to know what the isInCityRadius() function does.
 
Back
Top Bottom