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