Gedemon's Civilization, development thread

those small notification sounds means a warning is detected (and written in the log)

the barbarian sound is used for error (which are written in the log and displayed on screen to be sure)

helps me to check what's logged when running long autoplay session.
 
Small update
Code:
- bug fix : Large Dry Dock requires Harbor and Seaport (bonus: fix name, icons)
- bug fix : Correct initialization of cities with size > 1
- balance : more Lower Housing, less Upper Housing
- balance : change Tanks and Modern Armor desirability
 
Update
Code:
- add Air units industry (Buildings/Resources/Equipment)
 
Small update
Code:
- balance : initialize new cities at full stock for food
- bug fix : get correct Personnel requirement from Military Organization Level to check if the Recruitment Centre building can be constructed
 
Some discussions in the Ideas & Suggestions section have made me think again on some possibilities on the gameplay side in relation to Empire management (hi @Trav'ling Canuck @Boris Gudenuf and @HorseshoeHermit)

First, a reminder, I'm not against micromanagement, on the contrary, I want to allow a lot of it in the mod, but I strongly believe that you should be able to play a complete game "casually" without needing to micromanage anything.

Now this is more about limiting and unlocking micromanaging, depending on your governement type and your technical advancement, the control (has a human player) you have over your units and cities may be limited.

Cities that are too far or doesn't have a trade route (road, river, sea) with your capital may use an automated build list.

Units going too far away from your capital/cities may use a (simplified) AI when losing their supply link.

The AI players would not be affected in the same way, first because I don't have a way to take over control of their units, but also because they are always under an AI control, which, from the multiple threads on the subject, is not as good as the human player (but much better than what it was at release !)

As said previously, an AI related to the mod for city building list will be used at some point, then limitation by governement/policies/era may be used on it too.

Using the code required for the above, automating a city (or even units) by choice (even if your "administration level" allows direct control) would also be possible, helping late game management.

As discussed in the thread linked at the top of this post, later Era policies/governement may also bring new limitations to shift management from local to global.

Now, I don't want to implement too many mechanism as options, but for this one I may consider it, as it may be controversial.

I mean, I loved the "your governement type doesn't allow you to declare war against this civ at this time" from a previous version of the game, but I also remember that some others hated it.
 
Ho, right, sorry and hi @Naokaukodem !

And another reason for my apologies, yesterday update broke the mod, this one should fix it:
Code:
- fix typo in previous upload that prevented the CityScript to load (and crashed both the UI and all scripts) 
- add FontIcon for Avionic
- copy all Population types to the Resources table, to unify the loading/saving/update of all "stock" and simplify the global code
 
Copied from bug report section:

I'm just realizing that the latest update may have broken saved game from previous version (rural population is stocked/saved in a different table), and could have reset the population on plots to starting game's values.

In later era the cities provide too much employment to give an incentive for urban population to move on plots outside outside cities. This is wanted, but combined with the above issue may broke late game saves.

Future updates will include more push/pull to force urban population to migrate (housing for example)
 
Update on GitHub
Code:
- add/update migration code from Cities to Cities, Plots to Cities, Cities to Plots
- update Culture values on Migration
- add variation values for Population and Culture on plots ToolTip

Spoiler Variations shown on plots ToolTip :
Clipboard-1.jpg


Starting games in later Eras should now be possible with the updated migration code.

Migration is currently done on Push/Pull values for Food, Housing, Employment, Threats will come next, other parameters are planned.

Migration from cities use more parameters than from plots, to prevent overload on larger maps. It use multiple pass in a city, one for each population class level, each class being affected differently by the push/pull factor and the type/distance of migration.

For those interested, the code in cities below:
Code:
local migrationClassMotivation    = {
    -- Main motivations
    ["Employment"]     = { [UpperClassID]     = 0.10, [MiddleClassID] = 2.00, [LowerClassID]     = 3.00, },
    ["Housing"]     = { [UpperClassID]     = 2.00, [MiddleClassID] = 0.75, [LowerClassID]     = 0.50, },
    ["Food"]         = { [UpperClassID]     = 0.25, [MiddleClassID] = 2.00, [LowerClassID]     = 3.00, },
    ["Threat"]         = { [UpperClassID]     = 3.00, [MiddleClassID] = 2.00, [LowerClassID]     = 1.00, },
    -- Values below are used for further calculation
    ["Rural"]         = { [UpperClassID]     = 0.25, [MiddleClassID] = 1.00, [LowerClassID]     = 1.00, },    -- Moving away from cities
    ["Urban"]         = { [UpperClassID]     = 2.00, [MiddleClassID] = 1.00, [LowerClassID]     = 1.00, },    -- Moving to other cities
    ["Emigration"]     = { [UpperClassID]     = 1.00, [MiddleClassID] = 0.75, [LowerClassID]     = 0.50, },    -- Moving to foreign cities
    ["Transport"]     = { [UpperClassID]     = 1.00, [MiddleClassID] = 0.50, [LowerClassID]     = 0.10, },    -- Ability to move over distance (max = 1.00) (todo : era dependant)
}

function SetMigrationValues(self)

    Dlog("SetMigrationValues ".. Locale.Lookup(self:GetName()).." /START")
    local DEBUG_CITY_SCRIPT     = DEBUG_CITY_SCRIPT
    if Game.GetLocalPlayer()     == self:GetOwner() then DEBUG_CITY_SCRIPT = "debug" end
    
    local cityKey = self:GetKey()
    if not _cached[cityKey] then
        _cached[cityKey] = {}
    end
    if not _cached[cityKey].Migration then
        _cached[cityKey].Migration = {
            Push         = {Employment = {}, Housing = {}, Food = {}},
            Pull         = {Employment = {}, Housing = {}, Food = {}},
            Migrants     = {Employment = {}, Housing = {}, Food = {}},
            Motivation    = {}
        }
    end
    local cityMigration = _cached[cityKey].Migration
    
    Dprint( DEBUG_CITY_SCRIPT, GCO.Separator)
    Dprint( DEBUG_CITY_SCRIPT, "- Set Migration values for ".. Locale.Lookup(self:GetName()))
    local possibleDestination         = {}
    local migrantClasses            = {UpperClassID, MiddleClassID, LowerClassID}
    local housingID                    = { [UpperClassID]     = YieldUpperHousingID, [MiddleClassID] = YieldMiddleHousingID, [LowerClassID]     = YieldLowerHousingID, } -- to do : something else...
    --local migrantMotivations        = {"Under threat", "Starvation", "Employment", "Overpopulation"}
    local migrants                    = {}
    local totalPopulation             = self:GetUrbanPopulation()
    
    -- for now global, but todo : per class employment in cities
    local employment                = self:GetMaxEmploymentUrban()
    local employed                    = self:GetUrbanEmployed()
    local unEmployed                = math.max(0, totalPopulation - employment)
    
    local classesRatio                = {}
    for i, classID in ipairs(migrantClasses) do
        classesRatio[classID] = self:GetPopulationClass(classID) / totalPopulation
    end
    
    Dprint( DEBUG_CITY_SCRIPT, "  - UnEmployed = ", unEmployed," employment : ", employment, " totalPopulation = ", totalPopulation)
    
    for _, populationID in pairs(migrantClasses) do
    
        local population            = self:GetPopulationClass(populationID)       
        local housingSize            = self:GetCustomYield( housingID[populationID] )
        local maxPopulation            = GetPopulationPerSize(housingSize)
        local bestMotivationWeight    = 0
        
        Dprint( DEBUG_CITY_SCRIPT, "  - "..Indentation20(Locale.ToUpper(GameInfo.Resources[populationID].Name)).." current population = "..Indentation15(population).. " motivations : employment = ".. tostring(migrationClassMotivation.Employment[populationID]) ..", housing = ".. migrationClassMotivation.Housing[populationID] ..", food = ".. tostring(migrationClassMotivation.Food[populationID]))
        
        if population > 0 then
            -- check Migration motivations, from lowest to most important :   
        
            -- Employment
            -- for now global, but todo : per class employment in cities
            if employment > 0 then
                cityMigration.Pull.Employment[populationID]        = employment / totalPopulation
                cityMigration.Push.Employment[populationID]        = totalPopulation / employment
                cityMigration.Migrants.Employment[populationID]    = 0
                if cityMigration.Push.Employment[populationID] > 1 then
                    local motivationWeight = cityMigration.Push.Employment[populationID] * migrationClassMotivation.Employment[populationID]
                    if motivationWeight > bestMotivationWeight then
                        cityMigration.Motivation[populationID]        = "Employment"
                        bestMotivationWeight                        = motivationWeight
                    end
                    cityMigration.Migrants.Employment[populationID]    = math.floor(unEmployed * classesRatio[populationID] * math.min(1, migrationClassMotivation.Employment[populationID])) -- Weight affect numbers of migrant when < 1.00
                end
            else
                cityMigration.Pull.Employment[populationID]    = 0
                cityMigration.Push.Employment[populationID]    = 0       
            end
            Dprint( DEBUG_CITY_SCRIPT, "  - Employment migrants for ...."..Indentation20(Locale.Lookup(GameInfo.Resources[populationID].Name)).." = ".. tostring(cityMigration.Migrants.Employment[populationID]) .."/".. population )
            
            -- Housing
            cityMigration.Pull.Housing[populationID]        = maxPopulation / population
            cityMigration.Push.Housing[populationID]        = population / maxPopulation
            cityMigration.Migrants.Housing[populationID]    = 0
            if cityMigration.Push.Housing[populationID] > 1 then
                local motivationWeight = cityMigration.Push.Housing[populationID] * migrationClassMotivation.Housing[populationID]
                if motivationWeight > bestMotivationWeight then
                    cityMigration.Motivation[populationID]        = "Housing"
                    bestMotivationWeight                        = motivationWeight
                end
                local overPopulation                            = population - maxPopulation
                cityMigration.Migrants.Housing[populationID]    = math.floor(overPopulation * math.min(1, migrationClassMotivation.Housing[populationID]))
            end
            --Dprint( DEBUG_CITY_SCRIPT, "  - Overpopulation = ", overPopulation," maxPopulation : ", maxPopulation, " population = ", population)
            Dprint( DEBUG_CITY_SCRIPT, "  - Overpopulation migrants for "..Indentation20(Locale.Lookup(GameInfo.Resources[populationID].Name)).." = ".. tostring(cityMigration.Migrants.Housing[populationID]) .."/".. population )
            
            -- Starvation
            -- Todo : get values per population class from NeedsEffects instead of global values here
            local consumptionRatio                        = 1
            local foodNeeded                            = self:GetFoodConsumption(consumptionRatio)
            local foodstock                                = self:GetFoodStock()
            cityMigration.Pull.Food[populationID]        = foodstock / foodNeeded
            cityMigration.Push.Food[populationID]        = foodNeeded / foodstock
            cityMigration.Migrants.Food[populationID]    = 0
            
            if cityMigration.Push.Food[populationID] > 1 then
                local motivationWeight = cityMigration.Push.Food[populationID] * migrationClassMotivation.Food[populationID]
                if motivationWeight >= bestMotivationWeight then
                    cityMigration.Motivation[populationID]        = "Food"
                    bestMotivationWeight                        = motivationWeight
                end
                local starving                                = population - (population / cityMigration.Push.Food[populationID])
                cityMigration.Migrants.Food[populationID]    = math.floor(starving * classesRatio[populationID] * math.min(1, migrationClassMotivation.Food[populationID]))
            end
            --Dprint( DEBUG_CITY_SCRIPT, "  - Starving = ", starving," foodNeeded : ", foodNeeded, " foodstock = ", foodstock)
            Dprint( DEBUG_CITY_SCRIPT, "  - Starving migrants for ......"..Indentation20(Locale.Lookup(GameInfo.Resources[populationID].Name)).." = ".. tostring(cityMigration.Migrants.Food[populationID]) .."/".. population )

            -- Threat
            --
            --
            
            Dprint( DEBUG_CITY_SCRIPT, "  - Best motivation : ", cityMigration.Motivation[populationID])
            Dprint( DEBUG_CITY_SCRIPT, "  - Pull.Food ......: ", GCO.ToDecimals(cityMigration.Pull.Food[populationID]),         " Push.Food ......= ", GCO.ToDecimals(cityMigration.Push.Food[populationID]))
            Dprint( DEBUG_CITY_SCRIPT, "  - Pull.Housing ...: ", GCO.ToDecimals(cityMigration.Pull.Housing[populationID]),         " Push.Housing ...= ", GCO.ToDecimals(cityMigration.Push.Housing[populationID]))
            Dprint( DEBUG_CITY_SCRIPT, "  - Pull.Employment : ", GCO.ToDecimals(cityMigration.Pull.Employment[populationID]),     " Push.Employment = ", GCO.ToDecimals(cityMigration.Push.Employment[populationID]))
        end
    end
    Dlog("SetMigrationValues ".. Locale.Lookup(self:GetName()).." /END")
end

function DoMigration(self)

    Dlog("DoMigration ".. Locale.Lookup(self:GetName()).." /START")
    
    local DEBUG_CITY_SCRIPT     = DEBUG_CITY_SCRIPT
    if Game.GetLocalPlayer()     == self:GetOwner() then DEBUG_CITY_SCRIPT = "debug" end
    
    Dprint( DEBUG_CITY_SCRIPT, GCO.Separator)
    Dprint( DEBUG_CITY_SCRIPT, "- Population Migration for ".. Locale.Lookup(self:GetName()))
    
    local migrantClasses        = {UpperClassID, MiddleClassID, LowerClassID}
    
    -- Get potential migrants
    local totalPopulation         = self:GetRealPopulation()
    local minPopulationLeft        = GetPopulationPerSize(math.max(1, self:GetSize()-1))
    local availableMigrants        = totalPopulation - minPopulationLeft
    
    if availableMigrants  > 0 then
        local maxMigrants        = math.floor(availableMigrants * maxMigrantPercent / 100)
        local minMigrants        = math.floor(availableMigrants * minMigrantPercent / 100)
        
        --local migrants            = math.min(maxMigrants, minMigrants)
        
        Dprint( DEBUG_CITY_SCRIPT, "  - Max Migrants = ", maxMigrants, " Min Migrants = ", minMigrants)
        
        if maxMigrants > 0 then
        
            local cityMigration             = self:GetMigration()
            
            local classesRatio                = {}
            for i, classID in ipairs(migrantClasses) do
                classesRatio[classID] = self:GetPopulationClass(classID) / totalPopulation
            end
            
            -- Do Migration for each population class
            for _, populationID in ipairs(migrantClasses) do
            
                local possibleDestination         = {}
                local majorMotivation            = cityMigration.Motivation[populationID] or "Greener Pastures" -- to do: check if this happens
                local bestMotivationValue        = 0
                local bestMotivationWeight        = 0
                local totalWeight                = 0
                local migrants                    = 0
            
                -- Get the number of migrants for each class from this city
                for motivation, value in pairs(cityMigration.Migrants) do
                    -- motivations can overlap, so just use the biggest value for this populationID from all motivations
                    migrants = math.max(value[populationID], migrants )
                end
                -- Max/Min migrant for a populationID are relative to the populationID ratio, to prevent all migrants
                -- to be of the same populationID when that class has more eager migrants than the max possible value for the whole city population
                migrants = math.floor(math.min(maxMigrants * classesRatio[populationID], math.max(minMigrants * classesRatio[populationID], migrants)))
                Dprint( DEBUG_CITY_SCRIPT, "  - Eager migrants for "..Indentation20(Locale.ToUpper(GameInfo.Resources[populationID].Name)).." = ".. tostring(migrants) .."/".. tostring(self:GetPopulationClass(populationID)) .. ", Major motivation = " .. tostring(majorMotivation) )
            
                -- Get possible destinations from this city own plots
                local cityPlots    = GCO.GetCityPlots(self)
                for _, plotID in ipairs(cityPlots) do
                    local plot = GCO.GetPlotByIndex(plotID)
                    if plot and (not plot:IsCity()) then
                        local plotMigration = plot:GetMigration()
                        if plotMigration then
                            local distance        = Map.GetPlotDistance(self:GetX(), self:GetY(), plot:GetX(), plot:GetY())
                            local efficiency     = (1 - math.min(0.9, distance / 10)) * migrationClassMotivation["Transport"][populationID]
                            local factor        = migrationClassMotivation["Rural"][populationID] * efficiency
                            local plotWeight     = 0
                            if majorMotivation == "Greener Pastures" then                               
                                plotWeight = 1  -- Any plots will have mimimun attraction for adventurers
                            end
                            Dprint( DEBUG_CITY_SCRIPT, "   - Looking for better conditions on plot (".. plot:GetX() ..",".. plot:GetY().."), Class Factor = ", GCO.ToDecimals(factor))
                            for motivation, pushValues in pairs(cityMigration.Push) do
                                local pushValue        = pushValues[populationID]
                                local weightRatio    = migrationClassMotivation[motivation][populationID] * factor
                                local plotPull        = plotMigration.Pull[motivation] or 0
                                local plotPush        = plotMigration.Push[motivation] or 0
                                -- special case here : use housing plotPull for food motivation (which is the same in a city and its plots) as sending people
                                -- in plots which could maintain more population if they were not attached to a city may help produce more food for the region
                                if motivation == "Food" then
                                    plotPull        = plotMigration.Pull["Housing"] or 0
                                end                               
                                Dprint( DEBUG_CITY_SCRIPT, "     -  Motivation : "..Indentation15(motivation) .. " pushValue = ", GCO.ToDecimals(pushValue), " plotPush = ", GCO.ToDecimals(plotPush), " plotPull = ", GCO.ToDecimals(plotPull))
                                if plotPush < pushValue then             -- situation is better on adjacentPlot than on currentPlot for [motivation]
                                    if plotPull > 1 then
                                        weightRatio = weightRatio * 2        -- situation is good on adjacentPlot
                                    end
                                    if pushValue > 1 then
                                        weightRatio = weightRatio * 5        -- situation is bad on currentPlot
                                    end
                                    if motivation == majorMotivation then
                                        weightRatio = weightRatio * 10        -- this is the most important motivation for migration
                                    end
                                    local motivationWeight = (plotPull + pushValue) * weightRatio
                                    plotWeight = plotWeight + motivationWeight
                                    Dprint( DEBUG_CITY_SCRIPT, "       -  weightRatio = ", GCO.ToDecimals(weightRatio), " motivationWeight = ", GCO.ToDecimals(motivationWeight), " updated plotWeight = ", GCO.ToDecimals(plotWeight))
                                end               
                            end

                            if plotWeight > 0 then
                                totalWeight = totalWeight + plotWeight
                                table.insert (possibleDestination, {PlotID = plot:GetIndex(), Weight = plotWeight, MigrationEfficiency = efficiency})
                            end
                        else
                            GCO.Warning("plotMigration is nil for plot @(".. tostring(plot:GetX())..",".. tostring(plot:GetY())..")")
                        end
                    end
                end
                
                -- Get possible destinations from transfer cities
                local data     = self:GetTransferCities() or {}
                for routeCityKey, routeData in pairs(data) do
                    local city    = GetCityFromKey(routeCityKey)
                    if city then

                        local otherCityMigration = city:GetMigration()
                        if otherCityMigration then
                            local cityWeight     = 0
                            local efficiency    = routeData.Efficiency / 100 * migrationClassMotivation["Transport"][populationID]
                            local factor        = migrationClassMotivation["Urban"][populationID] * efficiency
                            if majorMotivation == "Greener Pastures" then                               
                                cityWeight = 1  -- Any city will have mimimun attraction for adventurers
                            end
                            Dprint( DEBUG_CITY_SCRIPT, "   - Looking for better conditions in City of ".. Locale.Lookup(city:GetName()), " Class Factor = ", GCO.ToDecimals(factor))
                            for motivation, pushValues in pairs(cityMigration.Push) do
                                local pushValue        = pushValues[populationID]
                                local weightRatio    = migrationClassMotivation[motivation][populationID] * factor
                                local cityPull        = otherCityMigration.Pull[motivation][populationID] or 0
                                local cityPush        = otherCityMigration.Push[motivation][populationID] or 0
                            
                                Dprint( DEBUG_CITY_SCRIPT, "     -  Motivation : "..Indentation15(motivation) .. " pushValue = ", GCO.ToDecimals(pushValue), " cityPush = ", GCO.ToDecimals(cityPush), " cityPull = ", GCO.ToDecimals(cityPull))
                                if cityPush < pushValue then                 -- situation is better in other city than in current city for [motivation]
                                    if cityPull > 1 then
                                        weightRatio = weightRatio * 2        -- situation is good in other city
                                    end
                                    if pushValue > 1 then
                                        weightRatio = weightRatio * 5        -- situation is bad in current city
                                    end
                                    if motivation == majorMotivation then
                                        weightRatio = weightRatio * 10        -- this is the most important motivation for migration
                                    end
                                    local motivationWeight = (cityPull + pushValue) * weightRatio
                                    cityWeight = cityWeight + motivationWeight
                                    Dprint( DEBUG_CITY_SCRIPT, "       -  weightRatio = ", GCO.ToDecimals(weightRatio), " motivationWeight = ", GCO.ToDecimals(motivationWeight), " updated cityWeight = ", GCO.ToDecimals(cityWeight))
                                end
                            end

                            if cityWeight > 0 then
                                totalWeight = totalWeight + cityWeight
                                table.insert (possibleDestination, {City = city, Weight = cityWeight, MigrationEfficiency = efficiency})
                            end
                        else
                            GCO.Warning("cityMigration is nil for ".. Locale.Lookup(city:GetName()))
                        end
                    end
                end
                
                -- Get possible destinations from foreign cities
                local data     = self:GetExportCities() or {}
                for routeCityKey, routeData in pairs(data) do
                    local city    = GetCityFromKey(routeCityKey)
                    if city then

                        local otherCityMigration = city:GetMigration()
                        if otherCityMigration then
                            local cityWeight     = 0
                            local efficiency    = routeData.Efficiency / 100 * migrationClassMotivation["Transport"][populationID]
                            local factor        = migrationClassMotivation["Emigration"][populationID] * efficiency
                            if majorMotivation == "Greener Pastures" then                               
                                cityWeight = 1  -- Any city will have mimimun attraction for adventurers
                            end
                            Dprint( DEBUG_CITY_SCRIPT, "   - Looking for better conditions in foreign City of ".. Locale.Lookup(city:GetName()), " Class Factor = ", GCO.ToDecimals(factor))
                            for motivation, pushValues in pairs(cityMigration.Push) do
                                local pushValue        = pushValues[populationID]
                                local weightRatio    = migrationClassMotivation[motivation][populationID] * factor
                                local cityPull        = otherCityMigration.Pull[motivation][populationID] or 0
                                local cityPush        = otherCityMigration.Push[motivation][populationID] or 0
                            
                                Dprint( DEBUG_CITY_SCRIPT, "     -  Motivation : "..Indentation15(motivation) .. " pushValue = ", GCO.ToDecimals(pushValue), " cityPush = ", GCO.ToDecimals(cityPush), " cityPull = ", GCO.ToDecimals(cityPull))
                                if cityPush < pushValue then                 -- situation is better in other city than in current city for [motivation]
                                    if cityPull > 1 then
                                        weightRatio = weightRatio * 2        -- situation is good in other city
                                    end
                                    if pushValue > 1 then
                                        weightRatio = weightRatio * 5        -- situation is bad in current city
                                    end
                                    if motivation == majorMotivation then
                                        weightRatio = weightRatio * 10        -- this is the most important motivation for migration
                                    end
                                    local motivationWeight = (cityPull + pushValue) * weightRatio
                                    cityWeight = cityWeight + motivationWeight
                                    Dprint( DEBUG_CITY_SCRIPT, "       -  weightRatio = ", GCO.ToDecimals(weightRatio), " motivationWeight = ", GCO.ToDecimals(motivationWeight), " updated cityWeight = ", GCO.ToDecimals(cityWeight))
                                end
                            end

                            if cityWeight > 0 then
                                totalWeight = totalWeight + cityWeight
                                table.insert (possibleDestination, {City = city, Weight = cityWeight, MigrationEfficiency = efficiency})
                            end
                        else
                            GCO.Warning("cityMigration is nil for ".. Locale.Lookup(city:GetName()))
                        end
                    end
                end
            
                -- Migrate to best destinations
                table.sort(possibleDestination, function(a, b) return a.Weight > b.Weight; end)
                local numPlotDest = #possibleDestination
                for i, destination in ipairs(possibleDestination) do
                    if migrants > 0 and destination.Weight > 0 then
                        -- MigrationEfficiency already affect destination.Weight, but when there is not many possible destination
                        -- we want to limit the number of migrants over long routes, so it's included here too
                        local popMoving     = math.floor(migrants * (destination.Weight / totalWeight) * destination.MigrationEfficiency)
                        if popMoving > 0 then
                            local originePlot    = GCO.GetPlot(self:GetX(), self:GetY())
                            if destination.PlotID then
                                local plot     = GCO.GetPlotByIndex(destination.PlotID)
                                Dprint( DEBUG_CITY_SCRIPT, "- Moving " .. Indentation20(tostring(popMoving) .. " " ..Locale.Lookup(GameInfo.Resources[populationID].Name)).. " to plot ("..tostring(plot:GetX())..","..tostring(plot:GetY())..") with Weight = "..tostring(destination.Weight))
                                self:ChangePopulationClass(populationID, -popMoving)
                                plot:ChangePopulationClass(populationID, popMoving)
                                originePlot:DiffuseCultureFromMigrationTo(plot, popMoving)
                            else
                                local city     = destination.City
                                local plot    = GCO.GetPlot(city:GetX(), city:GetY())
                                Dprint( DEBUG_CITY_SCRIPT, "- Moving " .. Indentation20(tostring(popMoving) .. " " ..Locale.Lookup(GameInfo.Resources[populationID].Name)).. " to city ("..Locale.Lookup(city:GetName())..") with Weight = "..tostring(destination.Weight))
                                self:ChangePopulationClass(populationID, -popMoving)
                                city:ChangePopulationClass(populationID, popMoving)
                                originePlot:DiffuseCultureFromMigrationTo(plot, popMoving)
                            end
                        end
                    end   
                end
            end
        end
    end
    
    Dlog("DoMigration ".. Locale.Lookup(self:GetName()).." /END")
end

and "simplified" for plots (but using different values based on terrain types, feature types, and following rivers/roads)
Code:
function SetMigrationValues(self)

    if self:IsCity() then return end -- cities handle migration differently
        
    local DEBUG_PLOT_SCRIPT    = DEBUG_PLOT_SCRIPT
    --if self:GetOwner() == Game.GetLocalPlayer() then DEBUG_PLOT_SCRIPT = "debug" end
    
    local plotKey = self:GetKey()
    if not _cached[plotKey] then
        _cached[plotKey] = {}
    end
    if not _cached[plotKey].Migration then
        _cached[plotKey].Migration = { Push = {}, Pull = {}, Migrants = {}}
    end
    
    local plotMigration = _cached[plotKey].Migration
    
    Dprint( DEBUG_PLOT_SCRIPT, GCO.Separator)
    Dprint( DEBUG_PLOT_SCRIPT, "- Set Migration values to plot ".. self:GetX() ..",".. self:GetY())
    local possibleDestination         = {}
    local city                        = self:GetCity()
    local migrantClasses            = {UpperClassID, MiddleClassID, LowerClassID}
    --local migrantMotivations        = {"Under threat", "Starvation", "Employment", "Overpopulation"}
    local migrants                    = {}
    local population                = self:GetPopulation()
    local maxPopulation                = GCO.GetPopulationAtSize(self:GetMaxSize())
    local employment                = self:GetMaxEmployment()
    local employed                    = self:GetEmployed()
    local unEmployed                = math.max(0, population - employment)
    
    if population > 0 then
        -- check Migration motivations, from lowest to most important :   
    
        -- Employment
        if employment > 0 then
            plotMigration.Pull.Employment    = employment / population
            plotMigration.Push.Employment    = population / employment
            if plotMigration.Push.Employment > 1 then
                plotMigration.Motivation             = "Employment"
                plotMigration.Migrants.Employment    = unEmployed
            end
        else
            plotMigration.Pull.Employment    = 0
            plotMigration.Push.Employment    = 0       
        end
        Dprint( DEBUG_PLOT_SCRIPT, "  - UnEmployed = ", unEmployed," employment : ", employment, " population = ", population)
        
        -- Population
        plotMigration.Pull.Housing    = maxPopulation / population
        plotMigration.Push.Housing    = population / maxPopulation   
        if plotMigration.Push.Housing > 1 then
            plotMigration.Motivation             = "Population"
            local overPopulation                = population - maxPopulation
            plotMigration.Migrants.Housing    = overPopulation
        end
        Dprint( DEBUG_PLOT_SCRIPT, "  - Overpopulation = ", overPopulation," maxPopulation : ", maxPopulation, " population = ", population)
        
        -- Starvation
        if city then
            -- starvation can happen on plots controlled by a city (the city is requisitionning all food then share it over its urban+rural population)
            -- on free plots, the risk of starvation is part of the "Overpopulation" motivation
            -- if food rationning in city, try to move to external plot (other civ or unowned), reason : "food is requisitioned"
            local consumptionRatio    = 1
            local foodNeeded        = city:GetFoodConsumption(consumptionRatio)
            local foodstock            = city:GetFoodStock()
            plotMigration.Pull.Food    = foodstock / foodNeeded
            plotMigration.Push.Food    = foodNeeded / foodstock
            if plotMigration.Push.Food > 1 then
                plotMigration.Motivation     = "Food"
                local starving                = population - (population / plotMigration.Push.Food)
                plotMigration.Migrants.Food    = starving
            end
            Dprint( DEBUG_PLOT_SCRIPT, "  - Starving = ", starving," foodNeeded : ", foodNeeded, " foodstock = ", foodstock)
        else
            plotMigration.Pull.Food    = plotMigration.Pull.Housing -- free plots use the population support value for Food when "pulling" migrants that are pushed out of a city influence by starvation
        end
        
        -- Threat
        --
        --
        
        if city then
        Dprint( DEBUG_PLOT_SCRIPT, "  - Pull.Food : ", GCO.ToDecimals(plotMigration.Pull.Food), " Push.Food = ", GCO.ToDecimals(plotMigration.Push.Food))
        end   
        Dprint( DEBUG_PLOT_SCRIPT, "  - Pull.Housing : ", GCO.ToDecimals(plotMigration.Pull.Housing), " Push.Housing = ", GCO.ToDecimals(plotMigration.Push.Housing))
        Dprint( DEBUG_PLOT_SCRIPT, "  - Pull.Employment : ", GCO.ToDecimals(plotMigration.Pull.Employment), " Push.Employment = ", GCO.ToDecimals(plotMigration.Push.Employment))
    end
end

function DoMigration(self)

    if self:IsCity() then return end -- cities handle migration differently
    
    local DEBUG_PLOT_SCRIPT    = DEBUG_PLOT_SCRIPT
    if self:GetOwner() == Game.GetLocalPlayer() then DEBUG_PLOT_SCRIPT = "debug" end   
    
    Dprint( DEBUG_PLOT_SCRIPT, GCO.Separator)
    Dprint( DEBUG_PLOT_SCRIPT, "- Population Migration from plot ".. self:GetX() ..",".. self:GetY())
    local plotKey                 = self:GetKey()
    local plotMigration         = _cached[plotKey].Migration
    local population            = self:GetPopulation()
    local possibleDestination     = {}
    local city                    = self:GetCity()
    local migrantClasses        = {UpperClassID, MiddleClassID, LowerClassID}
    --local migrantMotivations    = {"Under threat", "Starvation", "Employment", "Overpopulation"}   
    local maxMigrants            = math.floor(population * maxMigrantPercent / 100)
    local minMigrants            = math.floor(population * minMigrantPercent / 100)
    local migrants                 = 0
    local totalWeight            = 0
    
    local classesRatio            = {}
    for i, classID in ipairs(migrantClasses) do
        classesRatio[classID] = self:GetPopulationClass(classID) / population
    end
    -- Get the number of migrants from this plot
    for motivation, value in pairs(plotMigration.Migrants) do
        migrants = math.max(value, migrants) -- motivations can overlap, so just use the biggest value from all motivations
    end
    migrants = math.min(maxMigrants, math.max(minMigrants, migrants))
    
    Dprint( DEBUG_PLOT_SCRIPT, "- Eager migrants = ", migrants)
    for _, populationID in ipairs(migrantClasses) do
            
    end
    --]]
    
    if migrants > 0 then
        
        -- migration to adjacent plots
        for direction = 0, DirectionTypes.NUM_DIRECTION_TYPES - 1, 1 do
            local adjacentPlot         = Map.GetAdjacentPlot(self:GetX(), self:GetY(), direction)
            local diffusionValues    = self:GetPlotDiffusionValuesTo(direction)
            
            -- debug
            if (not diffusionValues) and adjacentPlot and not (adjacentPlot:IsCity() or adjacentPlot:IsWater()) then
                local toStr             = self:GetX() ..",".. self:GetY() .. " to " .. adjacentPlot:GetX() ..",".. adjacentPlot:GetY()
                local plotTerrainStr    = Locale.Lookup(GameInfo.Terrains[self:GetTerrainType()].Name)
                local toTerrainStr        = Locale.Lookup(GameInfo.Terrains[adjacentPlot:GetTerrainType()].Name)
                GCO.Warning("No diffusion value from ".. plotTerrainStr .. " " .. toStr .. " " .. toTerrainStr)
            end
            
            if diffusionValues and adjacentPlot and not (adjacentPlot:IsCity() or adjacentPlot:IsWater()) then
            
                local adjacentPlotKey         = adjacentPlot:GetKey()
                local adjacentPlotMigration = _cached[adjacentPlotKey].Migration
                local plotWeight            = 0
                Dprint( DEBUG_PLOT_SCRIPT, "  - Looking for better conditions in ".. DirectionString[direction] .." on plot ".. adjacentPlot:GetX() ..",".. adjacentPlot:GetY().." Diffusion Values : Bonus = "..tostring(diffusionValues.Bonus)..", Penalty = "..tostring(diffusionValues.Penalty)..", Ratio = "..tostring(diffusionValues.MaxRatio))
                for motivation, pushValue in pairs(plotMigration.Push) do
                    local adjacentPull    = adjacentPlotMigration.Pull[motivation] or 0
                    local adjacentPush    = adjacentPlotMigration.Push[motivation] or 0
                    -- to do : effect of owned / foreign / free plots
                    local weightRatio    = 1   
                    Dprint( DEBUG_PLOT_SCRIPT, "    -  Motivation : "..Indentation15(motivation) .. " pushValue = ", GCO.ToDecimals(pushValue), " adjacentPush = ", GCO.ToDecimals(adjacentPush), " adjacentPull = ", GCO.ToDecimals(adjacentPull))
                    if adjacentPush < pushValue then             -- situation is better on adjacentPlot than on currentPlot for [motivation]
                        if adjacentPull > 1 then
                            weightRatio = weightRatio * 2        -- situation is good on adjacentPlot
                        end
                        if pushValue > 1 then
                            weightRatio = weightRatio * 5        -- situation is bad on currentPlot
                        end
                        if motivation == plotMigration.Motivation then
                            weightRatio = weightRatio * 10        -- this is the most important motivation for migration
                        end
                        local motivationWeight = (adjacentPull + pushValue) * weightRatio
                        plotWeight = plotWeight + motivationWeight
                        Dprint( DEBUG_PLOT_SCRIPT, "       -  weightRatio = ", GCO.ToDecimals(weightRatio), " motivationWeight = ", GCO.ToDecimals(motivationWeight), " updated plotWeight = ", GCO.ToDecimals(plotWeight))
                    end
                end
                
                if plotWeight > 0 then
                    plotWeight = (plotWeight + diffusionValues.Bonus + diffusionValues.Penalty) * diffusionValues.MaxRatio
                    Dprint( DEBUG_PLOT_SCRIPT, "  - After diffusionValues: plotWeight = ", GCO.ToDecimals(plotWeight))
                    totalWeight = totalWeight + plotWeight
                    table.insert (possibleDestination, {PlotID = adjacentPlot:GetIndex(), Weight = plotWeight, MigrationEfficiency = diffusionValues.MaxRatio})
                end
            end
        end
        
        -- migration to owning city
        if city then
            
            local cityMigration = city:GetMigration()
            if cityMigration then
                local distance        = Map.GetPlotDistance(self:GetX(), self:GetY(), city:GetX(), city:GetY())
                local efficiency     = (1 - math.min(0.9, distance / 10))
                local cityWeight = 0
                Dprint( DEBUG_PLOT_SCRIPT, "  - Looking for better conditions in City of ".. Locale.Lookup(city:GetName()) ..", Transport efficiency = ", GCO.ToDecimals(efficiency))
                for motivation, pushValue in pairs(plotMigration.Push) do
                    local cityPull    = cityMigration.Pull[motivation][LowerClassID] or 0
                    local cityPush    = cityMigration.Push[motivation][LowerClassID] or 0
                    -- to do : effect of owned / foreign / free plots
                    local weightRatio    = efficiency   
                    Dprint( DEBUG_PLOT_SCRIPT, "    -  Motivation : "..Indentation15(motivation) .. " pushValue = ", GCO.ToDecimals(pushValue), " cityPush = ", GCO.ToDecimals(cityPush), " cityPull = ", GCO.ToDecimals(cityPull))
                    if cityPush < pushValue then             -- situation is better on adjacentPlot than on currentPlot for [motivation]
                        if cityPull > 1 then
                            weightRatio = weightRatio * 2        -- situation is good on adjacentPlot
                        end
                        if pushValue > 1 then
                            weightRatio = weightRatio * 5        -- situation is bad on currentPlot
                        end
                        if motivation == plotMigration.Motivation then
                            weightRatio = weightRatio * 10        -- this is the most important motivation for migration
                        end
                        local motivationWeight = (cityPull + pushValue) * weightRatio
                        cityWeight = cityWeight + motivationWeight
                        Dprint( DEBUG_PLOT_SCRIPT, "       -  weightRatio = ", GCO.ToDecimals(weightRatio), " motivationWeight = ", GCO.ToDecimals(motivationWeight), " updated cityWeight = ", GCO.ToDecimals(cityWeight))
                    end
                end
                
                if cityWeight > 0 then
                    totalWeight = totalWeight + cityWeight
                    table.insert (possibleDestination, {City = city, Weight = cityWeight, MigrationEfficiency = efficiency})
                end
            else
                GCO.Warning("cityMigration is nil for ".. Locale.Lookup(city:GetName()))
            end
        end
        
        table.sort(possibleDestination, function(a, b) return a.Weight > b.Weight; end)
        local numPlotDest = #possibleDestination
        for i, destination in ipairs(possibleDestination) do
            if migrants > 0 and destination.Weight > 0 then
                -- MigrationEfficiency already affect destination.Weight, but when there is not many possible destination
                -- we want to limit the number of migrants over difficult routes, so it's included here too
                local totalPopMoving     = math.floor(migrants * (destination.Weight / totalWeight) * destination.MigrationEfficiency)
                if totalPopMoving > 0 then
                    for i, classID in ipairs(migrantClasses) do
                        local classMoving = math.floor(totalPopMoving * classesRatio[classID])
                        if classMoving > 0 then
                            if destination.PlotID then
                                local plot = GCO.GetPlotByIndex(destination.PlotID)
                                Dprint( DEBUG_PLOT_SCRIPT, "- Moving " .. Indentation20(tostring(classMoving) .. " " ..Locale.Lookup(GameInfo.Resources[classID].Name)).. " to plot ("..tostring(plot:GetX())..","..tostring(plot:GetY())..") with Weight = "..tostring(destination.Weight))
                                self:ChangePopulationClass(classID, -classMoving)
                                plot:ChangePopulationClass(classID, classMoving)
                                self:DiffuseCultureFromMigrationTo(plot, classMoving)
                            else
                                local city = destination.City
                                local plot = GetPlot(city:GetX(), city:GetY())
                                Dprint( DEBUG_PLOT_SCRIPT, "- Moving " .. Indentation20(tostring(classMoving) .. " " ..Locale.Lookup(GameInfo.Resources[classID].Name)).. " to city ("..Locale.Lookup(city:GetName())..") with Weight = "..tostring(destination.Weight))
                                self:ChangePopulationClass(classID, -classMoving)
                                city:ChangePopulationClass(classID, classMoving)
                                self:DiffuseCultureFromMigrationTo(plot, classMoving)
                            end
                        end
                    end
                end
            end   
        end
    end
end
 
Small update
Code:
- bug fix: "OnNewTurn" events where called on the very first turn of a game starting in later eras, causing some initialization issues
- tweak Culture Diffusion from Migration, some values were off the charts
- tweak plots ToolTip for Culture Strength
 
Update
Code:
- balance : reduce Materiel cost of Aircraft factories and Large dry Dock
- balance : raise production of Electrical Devices from Copper
- bug fix : Large Dry Dock can now Stock its production of Electronic Systems and Advanced Electronic Systems used to produce large ships
- UI : add FontIcons and update the Buildings Tooltip for (I hope) a better presentation of the Buildings production
- bug fix : show Icon for Jet Engine Factory
- bug fix : add Air Strip and Missile Silo to the Builder Unit list of action
- tweak Culture change on Migration, still playing with numbers
- bug fix : MigrationEfficiency should never be > 1 (was trying to move more population from some plots than the current available population)

Spoiler New Buildings Tooltip :
Clipboard-1.jpg
Clipboard-2.jpg
Clipboard-3.jpg
 
Small update:
Code:
- balance : less Aluminum required for Fuselage
- balance : Electronic Factory use less copper for Electronic Components and Electrical Devices, and can stock Copper
- preparatory work on code for easier integration of new units
- bug fix : get correct MigrationEfficiency value from plots to plots (was always "1")
- bug fix : do not lose (global) Culture during Migration
 
Important update
Code:
- create "Conscripts" PromotionClass based on the proposed units tree* (miss Fusilier)
- update "Melee" (miss Grenadier) and "Skirmisher" (miss Scout with Rifles) PromotionClass
- recreate "Ranged" PromotionClass (first units only, up to Longbowman)
- add ArtDef for the new units
- add new Equipment: Stone-Tipped Spear, Trained Horses
- add new Equipment Classes : Primitive Spears (Wood, Stone) and Metal-Tipped Spears (Bronze, Iron) to differentiate early and later units using Spears
- balance : raise early Materiel production (from Stone/Wood)
- balance : raise (or add) Food Yield for some Buildings and Improvements
- new feature : Builders automatically collect resources from the plot they are on
- new feature : When using its last charge, a Builder send all resources it has collected (automatically or from Tribal Village / Barbarian Camps) to its linked City, if any
- new feature : Removing a Feature (Forest, Jungle, Marsh) with a Builder send a number of Resources to the builder's linked City (if any) based on the Feature's Resource and the Era
- balance : Cities doesn't remove all surplus at once, it now decays with a different rate if its used or not, relatively to the size of the surplus
- balance : Tweak some numbers related to Migration again, add MigrationWeight for Worked Plots

Some things may be broken, I've not tested the game past the two first Eras yet (and the Ranged line stop with Longbowman), but I'd like feedback on the early game with the new promotion lines (Conscripts, Skirmisher) and new gameplay features.

Let me know too if Chariot upgrade/downgrade is working (the new "Trained Horses" equipment is attached to them)

* the "proposed units tree" is here:
https://docs.google.com/spreadsheet..._yzdhqw6fDQRlfFoQoRzpJ92rI/edit#gid=469063905
 
Small update:
Code:
- balance : raise leather and some armors production ratio
- balance : remove raw resource requirement for naval units
- bug fix : tweak Armors and Trained Horses <Desirability> (also used as weight in units definition) to allows upgrade from Light Swordsman to Swordsman and Chariot to Riders
- bug fix : keep Chariot as an optional equipment of Riders to allow downgrade
 
Big update on the code side, preparing for what's next...

You'll need to update the Assets mod (mandatory) and YnAMP (from Steam or latest commit on GitHub, optional)

Code:
- add vital resources (Stone, Wheat or Rice) around starting locations (require updated YnAMP)
- link assets from "YnAMP - Basic Modern Civilizations" (mandatory update of the "Ged Assets" mod)
- replace all Civilization by a specific set of basic Civilizations ("YnAMP - Basic Modern Civilizations" is incompatible now)
- add Upgrade mechanism for Buildings (the old buildings give a bonus to build the new one, the new building removes the old ones)
- set <ObsoleteEra> property for some buildings
- add Bladesmith building (requires Blacksmith, and now only building to produce Swords)
- rename (medieval) "Market Extension" to "Large Market", add Supermarket and Hypermarket buildings for later Eras
- tweak stock and production of various building in relation to their upgrade path
- bug fix : make Slings obsolete at "Construction" tech instead of "Archery"
- remove Spiked Clubs equipment
- balance : less personnel (and so Horses/Chariot) required for Cavalry units
- add Dynamic Naming for Civilizations and Leaders (for now based on Government)
- balance : limit max employment values from the number of resources on a plot
- balance : plots with improvements now give a minimal amount of resource (related to the improvement), equal to the base plot's production
- balance : raise again the base number of resources on the map
- move the new Techs/Civics popup to the right of the screen
- tweak the Barbarian unit spawn list in relation to the new PromotionClasses (limited to the first eras)
- change the Error reports for data inconsistency and core/virtual HP desynchronization to be Warning reports instead
 
Small update
Code:
balance : less personnel required in early Skirmisher units
balance : raise employment limit from number of Resources
 
Small update
Code:
- bug fix : trade routes set by a trader units where not updated with diplomatic relations  (to set the type of resources traded  over it)
- balance : lower a bit the number of resources on plots, and make that number a bit more random
 
Currently working on the city banner, I want to separate the resources type in 4 lists (food, luxuries, strategic and equipment) and set the position of the icons for health, "population" (housing/employment/migration) and stability tooltips, I hope it's not too much...
Spoiler City banners :
Clipboard-1.jpg
 
Top Bottom