Assign Population Improvements (#6650)

City management UI to allow focusing automatic worker placement

Improvements to worker / specialist assignment routines
This commit is contained in:
itanasi 2022-05-19 15:12:23 -07:00 committed by GitHub
parent a272e8e7ba
commit a2bc1a1a29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 665 additions and 358 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1075,6 +1075,90 @@ TileSets/FantasyHex/Arrows/UnitHasAttacked
orig: 100, 60 orig: 100, 60
offset: 0, 0 offset: 0, 0
index: -1 index: -1
TileSets/Default/Arrows/Generic
rotate: false
xy: 4, 6
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/FantasyHex/Arrows/Generic
rotate: false
xy: 4, 6
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/Default/Arrows/UnitAttacked
rotate: false
xy: 190, 1164
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/FantasyHex/Arrows/UnitAttacked
rotate: false
xy: 190, 1164
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/Default/Arrows/UnitMoved
rotate: false
xy: 298, 1150
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/FantasyHex/Arrows/UnitMoved
rotate: false
xy: 298, 1150
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/Default/Arrows/UnitMoving
rotate: false
xy: 112, 6
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/FantasyHex/Arrows/UnitMoving
rotate: false
xy: 112, 6
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/Default/Arrows/UnitTeleported
rotate: false
xy: 622, 1228
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/FantasyHex/Arrows/UnitTeleported
rotate: false
xy: 622, 1228
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/Default/Arrows/UnitWithdrew
rotate: false
xy: 730, 1228
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/FantasyHex/Arrows/UnitWithdrew
rotate: false
xy: 730, 1228
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/Default/AtollOverlay TileSets/Default/AtollOverlay
rotate: false rotate: false
xy: 4, 830 xy: 4, 830
@ -1082,6 +1166,118 @@ TileSets/Default/AtollOverlay
orig: 100, 100 orig: 100, 100
offset: 0, 0 offset: 0, 0
index: -1 index: -1
TileSets/Default/Borders/ConcaveConvexInner
rotate: false
xy: 406, 1411
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/FantasyHex/Borders/ConcaveConvexInner
rotate: false
xy: 406, 1411
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/Default/Borders/ConcaveConvexOuter
rotate: false
xy: 406, 1165
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/FantasyHex/Borders/ConcaveConvexOuter
rotate: false
xy: 406, 1165
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/Default/Borders/ConcaveInner
rotate: false
xy: 622, 1205
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/FantasyHex/Borders/ConcaveInner
rotate: false
xy: 622, 1205
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/Default/Borders/ConcaveOuter
rotate: false
xy: 1378, 1273
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/FantasyHex/Borders/ConcaveOuter
rotate: false
xy: 1378, 1273
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/Default/Borders/ConvexConcaveInner
rotate: false
xy: 495, 1165
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/FantasyHex/Borders/ConvexConcaveInner
rotate: false
xy: 495, 1165
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/Default/Borders/ConvexConcaveOuter
rotate: false
xy: 711, 1205
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/FantasyHex/Borders/ConvexConcaveOuter
rotate: false
xy: 711, 1205
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/Default/Borders/ConvexInner
rotate: false
xy: 1378, 1250
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/FantasyHex/Borders/ConvexInner
rotate: false
xy: 1378, 1250
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/Default/Borders/ConvexOuter
rotate: false
xy: 1467, 1273
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/FantasyHex/Borders/ConvexOuter
rotate: false
xy: 1467, 1273
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/Default/CityOverlay TileSets/Default/CityOverlay
rotate: false rotate: false
xy: 1470, 1944 xy: 1470, 1944
@ -1089,6 +1285,34 @@ TileSets/Default/CityOverlay
orig: 100, 100 orig: 100, 100
offset: 0, 0 offset: 0, 0
index: -1 index: -1
TileSets/Default/Crosshair
rotate: false
xy: 482, 1944
size: 116, 100
orig: 116, 100
offset: 0, 0
index: -1
TileSets/FantasyHex/Crosshair
rotate: false
xy: 482, 1944
size: 116, 100
orig: 116, 100
offset: 0, 0
index: -1
TileSets/Default/CrosshatchHexagon
rotate: false
xy: 4, 1340
size: 273, 236
orig: 273, 236
offset: 0, 0
index: -1
TileSets/FantasyHex/CrosshatchHexagon
rotate: false
xy: 4, 1340
size: 273, 236
orig: 273, 236
offset: 0, 0
index: -1
TileSets/Default/FalloutOverlay TileSets/Default/FalloutOverlay
rotate: false rotate: false
xy: 590, 1836 xy: 590, 1836
@ -1110,6 +1334,20 @@ TileSets/Default/ForestOverlay
orig: 100, 100 orig: 100, 100
offset: 0, 0 offset: 0, 0
index: -1 index: -1
TileSets/Default/Highlight
rotate: false
xy: 4, 1832
size: 284, 212
orig: 284, 212
offset: 0, 0
index: -1
TileSets/FantasyHex/Highlight
rotate: false
xy: 4, 1832
size: 284, 212
orig: 284, 212
offset: 0, 0
index: -1
TileSets/Default/HillOverlay TileSets/Default/HillOverlay
rotate: false rotate: false
xy: 590, 1728 xy: 590, 1728
@ -1175,263 +1413,25 @@ TileSets/Default/Road
index: -1 index: -1
TileSets/Default/Tiles/River-Bottom TileSets/Default/Tiles/River-Bottom
rotate: false rotate: false
xy: 1574, 735 xy: 1534, 694
size: 32, 28 size: 32, 28
orig: 32, 28 orig: 32, 28
offset: 0, 0 offset: 0, 0
index: -1 index: -1
TileSets/Default/Tiles/River-BottomLeft TileSets/Default/Tiles/River-BottomLeft
rotate: false rotate: false
xy: 1574, 699 xy: 1534, 658
size: 32, 28 size: 32, 28
orig: 32, 28 orig: 32, 28
offset: 0, 0 offset: 0, 0
index: -1 index: -1
TileSets/Default/Tiles/River-BottomRight TileSets/Default/Tiles/River-BottomRight
rotate: false rotate: false
xy: 1574, 663 xy: 1534, 622
size: 32, 28 size: 32, 28
orig: 32, 28 orig: 32, 28
offset: 0, 0 offset: 0, 0
index: -1 index: -1
TileSets/FantasyHex/Arrows/Generic
rotate: false
xy: 4, 6
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/Default/Arrows/Generic
rotate: false
xy: 4, 6
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/FantasyHex/Arrows/UnitAttacked
rotate: false
xy: 190, 1164
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/Default/Arrows/UnitAttacked
rotate: false
xy: 190, 1164
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/FantasyHex/Arrows/UnitMoved
rotate: false
xy: 298, 1150
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/Default/Arrows/UnitMoved
rotate: false
xy: 298, 1150
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/FantasyHex/Arrows/UnitMoving
rotate: false
xy: 112, 6
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/Default/Arrows/UnitMoving
rotate: false
xy: 112, 6
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/FantasyHex/Arrows/UnitTeleported
rotate: false
xy: 622, 1228
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/Default/Arrows/UnitTeleported
rotate: false
xy: 622, 1228
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/FantasyHex/Arrows/UnitWithdrew
rotate: false
xy: 730, 1228
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/Default/Arrows/UnitWithdrew
rotate: false
xy: 730, 1228
size: 100, 60
orig: 100, 60
offset: 0, 0
index: -1
TileSets/FantasyHex/Borders/ConcaveConvexInner
rotate: false
xy: 406, 1411
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/Default/Borders/ConcaveConvexInner
rotate: false
xy: 406, 1411
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/FantasyHex/Borders/ConcaveConvexOuter
rotate: false
xy: 406, 1165
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/Default/Borders/ConcaveConvexOuter
rotate: false
xy: 406, 1165
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/FantasyHex/Borders/ConcaveInner
rotate: false
xy: 622, 1205
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/Default/Borders/ConcaveInner
rotate: false
xy: 622, 1205
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/FantasyHex/Borders/ConcaveOuter
rotate: false
xy: 1378, 1273
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/Default/Borders/ConcaveOuter
rotate: false
xy: 1378, 1273
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/FantasyHex/Borders/ConvexConcaveInner
rotate: false
xy: 495, 1165
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/Default/Borders/ConvexConcaveInner
rotate: false
xy: 495, 1165
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/FantasyHex/Borders/ConvexConcaveOuter
rotate: false
xy: 711, 1205
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/Default/Borders/ConvexConcaveOuter
rotate: false
xy: 711, 1205
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/FantasyHex/Borders/ConvexInner
rotate: false
xy: 1378, 1250
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/Default/Borders/ConvexInner
rotate: false
xy: 1378, 1250
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/FantasyHex/Borders/ConvexOuter
rotate: false
xy: 1467, 1273
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/Default/Borders/ConvexOuter
rotate: false
xy: 1467, 1273
size: 81, 15
orig: 81, 15
offset: 0, 0
index: -1
TileSets/FantasyHex/Crosshair
rotate: false
xy: 482, 1944
size: 116, 100
orig: 116, 100
offset: 0, 0
index: -1
TileSets/Default/Crosshair
rotate: false
xy: 482, 1944
size: 116, 100
orig: 116, 100
offset: 0, 0
index: -1
TileSets/FantasyHex/CrosshatchHexagon
rotate: false
xy: 4, 1340
size: 273, 236
orig: 273, 236
offset: 0, 0
index: -1
TileSets/Default/CrosshatchHexagon
rotate: false
xy: 4, 1340
size: 273, 236
orig: 273, 236
offset: 0, 0
index: -1
TileSets/FantasyHex/Highlight
rotate: false
xy: 4, 1832
size: 284, 212
orig: 284, 212
offset: 0, 0
index: -1
TileSets/Default/Highlight
rotate: false
xy: 4, 1832
size: 284, 212
orig: 284, 212
offset: 0, 0
index: -1
TileSets/FantasyHex/Railroad TileSets/FantasyHex/Railroad
rotate: false rotate: false
xy: 505, 1728 xy: 505, 1728
@ -2316,21 +2316,21 @@ TileSets/FantasyHex/Tiles/Quarry+Stone
index: -1 index: -1
TileSets/FantasyHex/Tiles/River-Bottom TileSets/FantasyHex/Tiles/River-Bottom
rotate: false rotate: false
xy: 1534, 694 xy: 1574, 735
size: 32, 28 size: 32, 28
orig: 32, 28 orig: 32, 28
offset: 0, 0 offset: 0, 0
index: -1 index: -1
TileSets/FantasyHex/Tiles/River-BottomLeft TileSets/FantasyHex/Tiles/River-BottomLeft
rotate: false rotate: false
xy: 1534, 658 xy: 1574, 699
size: 32, 28 size: 32, 28
orig: 32, 28 orig: 32, 28
offset: 0, 0 offset: 0, 0
index: -1 index: -1
TileSets/FantasyHex/Tiles/River-BottomRight TileSets/FantasyHex/Tiles/River-BottomRight
rotate: false rotate: false
xy: 1534, 622 xy: 1574, 663
size: 32, 28 size: 32, 28
orig: 32, 28 orig: 32, 28
offset: 0, 0 offset: 0, 0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -929,6 +929,8 @@ Nothing =
Annex city = Annex city =
Specialist Buildings = Specialist Buildings =
Specialist Allocation = Specialist Allocation =
Manual Specialists =
Auto Specialists =
Specialists = Specialists =
[specialist] slots = [specialist] slots =
Food eaten = Food eaten =
@ -956,6 +958,16 @@ Worked by [cityName] =
Lock = Lock =
Unlock = Unlock =
Move to city = Move to city =
Reset Citizens =
Citizen Management =
Avoid Growth =
Default Focus =
Food Focus =
Production Focus =
Gold Focus =
Science Focus =
Culture Focus =
Happiness Focus =
Please enter a new name for your city = Please enter a new name for your city =
Please select a tile for this building's [improvement] = Please select a tile for this building's [improvement] =

View File

@ -1,5 +1,6 @@
package com.unciv.logic.automation package com.unciv.logic.automation
import com.unciv.logic.city.CityFocus
import com.unciv.logic.city.CityInfo import com.unciv.logic.city.CityInfo
import com.unciv.logic.city.INonPerpetualConstruction import com.unciv.logic.city.INonPerpetualConstruction
import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.CivilizationInfo
@ -8,7 +9,6 @@ import com.unciv.logic.map.MapUnit
import com.unciv.logic.map.TileInfo import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap import com.unciv.logic.map.TileMap
import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.MilestoneType
import com.unciv.models.ruleset.Victory import com.unciv.models.ruleset.Victory
import com.unciv.models.ruleset.Victory.Focus import com.unciv.models.ruleset.Victory.Focus
import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.tile.ResourceType
@ -16,46 +16,94 @@ import com.unciv.models.ruleset.tile.TileImprovement
import com.unciv.models.ruleset.unique.LocalUniqueCache import com.unciv.models.ruleset.unique.LocalUniqueCache
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats import com.unciv.models.stats.Stats
import com.unciv.ui.victoryscreen.RankingType import com.unciv.ui.victoryscreen.RankingType
object Automation { object Automation {
fun rankTileForCityWork(tile: TileInfo, city: CityInfo, foodWeight: Float = 1f): Float { fun rankTileForCityWork(tile: TileInfo, city: CityInfo, cityStats: Stats): Float {
val stats = tile.getTileStats(city, city.civInfo) val stats = tile.getTileStats(city, city.civInfo)
return rankStatsForCityWork(stats, city, foodWeight) return rankStatsForCityWork(stats, city, cityStats)
} }
private fun rankStatsForCityWork(stats: Stats, city: CityInfo, foodWeight: Float = 1f): Float { fun rankSpecialist(specialist: String, cityInfo: CityInfo, cityStats: Stats): Float {
var rank = 0f val stats = cityInfo.cityStats.getStatsOfSpecialist(specialist)
if (city.population.population < 5) { var rank = rankStatsForCityWork(stats, cityInfo, cityStats, true)
// "small city" - we care more about food and less about global problems like gold science and culture // derive GPP score
rank += stats.food * 1.2f * foodWeight var gpp = 0f
rank += stats.production if (cityInfo.getRuleset().specialists.containsKey(specialist)) { // To solve problems in total remake mods
rank += stats.science / 2 val specialistInfo = cityInfo.getRuleset().specialists[specialist]!!
rank += stats.culture / 2 gpp = specialistInfo.greatPersonPoints.sumValues().toFloat()
rank += stats.gold / 5 // it's barely worth anything at this point
} else {
if (stats.food <= 2 || city.civInfo.getHappiness() > 5) rank += stats.food * 1.2f * foodWeight // food get more value to keep city growing
else rank += (2.4f + (stats.food - 2) / 2) * foodWeight // 1.2 point for each food up to 2, from there on half a point
if (city.civInfo.gold < 0 && city.civInfo.statsForNextTurn.gold <= 0)
rank += stats.gold // we have a global problem
else rank += stats.gold / 3 // 3 gold is worse than 2 production
rank += stats.production
rank += stats.science
if (city.tiles.size < 12 || city.civInfo.wantsToFocusOn(Victory.Focus.Culture)) {
rank += stats.culture
} else rank += stats.culture / 2
} }
gpp = gpp * (100 + cityInfo.currentGPPBonus) / 100
rank += gpp * 3 // GPP weight
return rank return rank
} }
internal fun rankSpecialist(stats: Stats, cityInfo: CityInfo): Float { private fun rankStatsForCityWork(stats: Stats, city: CityInfo, cityStats: Stats, specialist: Boolean = false): Float {
var rank = rankStatsForCityWork(stats, cityInfo) val cityAIFocus = city.cityAIFocus
rank += 0.3f //GPP bonus val yieldStats = stats.clone()
return rank
if (specialist) {
// If you have the Food Bonus, count as 1 extra food production (base is 2food)
for (unique in city.getMatchingUniques(UniqueType.FoodConsumptionBySpecialists))
if (city.matchesFilter(unique.params[1]))
yieldStats.food -= (unique.params[0].toFloat() / 100f) * 2f // base 2 food per Pop
// Specialist Happiness Percentage Change 0f-1f
for (unique in city.getMatchingUniques(UniqueType.UnhappinessFromPopulationTypePercentageChange))
if (city.matchesFilter(unique.params[2]) && unique.params[1] == "Specialists")
yieldStats.happiness -= (unique.params[0].toFloat() / 100f) // relative val is negative, make positive
if (city.civInfo.getHappiness() < 0) yieldStats.happiness *= 2 // double weight for unhappy civilization
}
val surplusFood = cityStats[Stat.Food]
// Apply base weights
yieldStats.applyRankingWeights()
if (surplusFood > 0 && city.avoidGrowth) {
yieldStats.food = 0f // don't need more food!
} else {
if (cityAIFocus != CityFocus.NoFocus && cityAIFocus != CityFocus.FoodFocus && cityAIFocus != CityFocus.ProductionGrowthFocus && cityAIFocus != CityFocus.GoldGrowthFocus) {
// Focus on non-food/growth
if (surplusFood < 0)
yieldStats.food *= 8 // Starving, need Food, get to 0
else
yieldStats.food /= 2
} else if (!city.avoidGrowth) {
// NoFocus or Food/Growth Focus. Target +2 Food Surplus
if (surplusFood < 2)
yieldStats.food *= 8
else if (cityAIFocus != CityFocus.FoodFocus)
yieldStats.food /= 2
if (city.population.population < 5 && cityAIFocus != CityFocus.FoodFocus)
// NoFocus or GoldGrow or ProdGrow, not Avoid Growth, pop < 5. FoodFocus already does this up
yieldStats.food *= 3
}
}
if (city.population.population < 5) {
// "small city" - we care more about food and less about global problems like gold science and culture
// Food already handled above. Science/Culture have low weights in Stats already
yieldStats.gold /= 2 // it's barely worth anything at this point
} else {
if (city.civInfo.gold < 0 && city.civInfo.statsForNextTurn.gold <= 0)
yieldStats.gold *= 2 // We have a global problem
if (city.tiles.size < 12 || city.civInfo.wantsToFocusOn(Focus.Culture))
yieldStats.culture *= 2
if (city.civInfo.getHappiness() < 0 && !specialist) // since this doesn't get updated, may overshoot
yieldStats.happiness *= 2
if (city.civInfo.wantsToFocusOn(Focus.Science))
yieldStats.science *= 2
}
// Apply City focus
cityAIFocus.applyWeightTo(yieldStats)
return yieldStats.values.sum()
} }
fun tryTrainMilitaryUnit(city: CityInfo) { fun tryTrainMilitaryUnit(city: CityInfo) {
@ -74,8 +122,8 @@ object Automation {
mapUnit.getMatchingUniques(UniqueType.CarryAirUnits).firstOrNull() ?: return 0 mapUnit.getMatchingUniques(UniqueType.CarryAirUnits).firstOrNull() ?: return 0
if (mapUnitCarryUnique.params[1] != carryFilter) return 0 //Carries a different type of unit if (mapUnitCarryUnique.params[1] != carryFilter) return 0 //Carries a different type of unit
return mapUnitCarryUnique.params[0].toInt() + return mapUnitCarryUnique.params[0].toInt() +
mapUnit.getMatchingUniques(UniqueType.CarryExtraAirUnits) mapUnit.getMatchingUniques(UniqueType.CarryExtraAirUnits)
.filter { it.params[1] == carryFilter }.sumOf { it.params[0].toInt() } .filter { it.params[1] == carryFilter }.sumOf { it.params[0].toInt() }
} }
val totalCarriableUnits = val totalCarriableUnits =
@ -97,13 +145,15 @@ object Automation {
findWaterConnectedCitiesAndEnemies.stepToEnd() findWaterConnectedCitiesAndEnemies.stepToEnd()
if (findWaterConnectedCitiesAndEnemies.getReachedTiles().none { if (findWaterConnectedCitiesAndEnemies.getReachedTiles().none {
(it.isCityCenter() && it.getOwner() != city.civInfo) (it.isCityCenter() && it.getOwner() != city.civInfo)
|| (it.militaryUnit != null && it.militaryUnit!!.civInfo != city.civInfo) || (it.militaryUnit != null && it.militaryUnit!!.civInfo != city.civInfo)
}) // there is absolutely no reason for you to make water units on this body of water. }) // there is absolutely no reason for you to make water units on this body of water.
militaryUnits = militaryUnits.filter { !it.isWaterUnit() } militaryUnits = militaryUnits.filter { !it.isWaterUnit() }
val carryingOnlyUnits = militaryUnits.filter { it.hasUnique(UniqueType.CarryAirUnits) val carryingOnlyUnits = militaryUnits.filter {
&& it.hasUnique(UniqueType.CannotAttack) }.toList() it.hasUnique(UniqueType.CarryAirUnits)
&& it.hasUnique(UniqueType.CannotAttack)
}.toList()
for (unit in carryingOnlyUnits) for (unit in carryingOnlyUnits)
if (providesUnneededCarryingSlots(unit, city.civInfo)) if (providesUnneededCarryingSlots(unit, city.civInfo))
@ -125,9 +175,11 @@ object Automation {
.map { it.unitType } .map { it.unitType }
.distinct() .distinct()
if (availableTypes.none()) return null if (availableTypes.none()) return null
val bestUnitsForType = availableTypes.map { type -> militaryUnits val bestUnitsForType = availableTypes.map { type ->
militaryUnits
.filter { unit -> unit.unitType == type } .filter { unit -> unit.unitType == type }
.maxByOrNull { unit -> unit.cost }!! } .maxByOrNull { unit -> unit.cost }!!
}
// Check the maximum force evaluation for the shortlist so we can prune useless ones (ie scouts) // Check the maximum force evaluation for the shortlist so we can prune useless ones (ie scouts)
val bestForce = bestUnitsForType.maxOf { it.getForceEvaluation() } val bestForce = bestUnitsForType.maxOf { it.getForceEvaluation() }
chosenUnit = bestUnitsForType.filter { it.uniqueTo != null || it.getForceEvaluation() > bestForce / 3 }.toList().random() chosenUnit = bestUnitsForType.filter { it.uniqueTo != null || it.getForceEvaluation() > bestForce / 3 }.toList().random()
@ -172,14 +224,14 @@ object Automation {
/** Checks both feasibility of Buildings with a CreatesOneImprovement unique /** Checks both feasibility of Buildings with a CreatesOneImprovement unique
* and resource scarcity making a construction undesirable. * and resource scarcity making a construction undesirable.
*/ */
fun allowAutomatedConstruction( fun allowAutomatedConstruction(
civInfo: CivilizationInfo, civInfo: CivilizationInfo,
cityInfo: CityInfo, cityInfo: CityInfo,
construction: INonPerpetualConstruction construction: INonPerpetualConstruction
): Boolean { ): Boolean {
return allowCreateImprovementBuildings(civInfo, cityInfo, construction) return allowCreateImprovementBuildings(civInfo, cityInfo, construction)
&& allowSpendingResource(civInfo, construction) && allowSpendingResource(civInfo, construction)
} }
/** Checks both feasibility of Buildings with a [UniqueType.CreatesOneImprovement] unique (appropriate tile available). /** Checks both feasibility of Buildings with a [UniqueType.CreatesOneImprovement] unique (appropriate tile available).
@ -241,7 +293,7 @@ object Automation {
val neededForBuilding = civInfo.lastEraResourceUsedForBuilding[resource] != null val neededForBuilding = civInfo.lastEraResourceUsedForBuilding[resource] != null
// Don't care about old units // Don't care about old units
val neededForUnits = civInfo.lastEraResourceUsedForUnit[resource] != null val neededForUnits = civInfo.lastEraResourceUsedForUnit[resource] != null
&& civInfo.lastEraResourceUsedForUnit[resource]!! >= civInfo.getEraNumber() && civInfo.lastEraResourceUsedForUnit[resource]!! >= civInfo.getEraNumber()
// No need to save for both // No need to save for both
if (!neededForBuilding || !neededForUnits) { if (!neededForBuilding || !neededForUnits) {
@ -283,11 +335,11 @@ object Automation {
} }
/** Support [UniqueType.CreatesOneImprovement] unique - find best tile for placement automation */ /** Support [UniqueType.CreatesOneImprovement] unique - find best tile for placement automation */
fun getTileForConstructionImprovement(cityInfo: CityInfo, improvement: TileImprovement): TileInfo? { fun getTileForConstructionImprovement(cityInfo: CityInfo, improvement: TileImprovement): TileInfo? {
return cityInfo.getTiles().filter { return cityInfo.getTiles().filter {
it.canBuildImprovement(improvement, cityInfo.civInfo) it.canBuildImprovement(improvement, cityInfo.civInfo)
}.maxByOrNull { }.maxByOrNull {
rankTileForCityWork(it, cityInfo) rankTileForCityWork(it, cityInfo, cityInfo.cityStats.currentCityStats)
} }
} }
@ -350,7 +402,7 @@ object Automation {
if (adjacentTile.hasViewableResource(cityInfo.civInfo) && if (adjacentTile.hasViewableResource(cityInfo.civInfo) &&
(adjacentDistance < 3 || (adjacentDistance < 3 ||
adjacentTile.tileResource.resourceType != ResourceType.Bonus adjacentTile.tileResource.resourceType != ResourceType.Bonus
) )
) score -= 1 ) score -= 1
if (adjacentTile.naturalWonder != null) { if (adjacentTile.naturalWonder != null) {
if (adjacentDistance < 3) adjacentNaturalWonder = true if (adjacentDistance < 3) adjacentNaturalWonder = true
@ -381,7 +433,7 @@ object Automation {
} }
} }
enum class ThreatLevel{ enum class ThreatLevel {
VeryLow, VeryLow,
Low, Low,
Medium, Medium,

View File

@ -853,7 +853,7 @@ object NextTurnAutomation {
city.annexCity() city.annexCity()
} }
city.reassignPopulation() city.reassignAllPopulation()
city.cityConstructions.chooseNextConstruction() city.cityConstructions.chooseNextConstruction()
if (city.health < city.getMaxHealth()) if (city.health < city.getMaxHealth())

View File

@ -19,6 +19,7 @@ import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.stats.Stat import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.collections.HashMap import kotlin.collections.HashMap
@ -34,6 +35,49 @@ enum class CityFlags {
Resistance Resistance
} }
// if tableEnabled == true, then Stat != null
enum class CityFocus(val label: String, val tableEnabled: Boolean, val stat: Stat? = null) {
NoFocus("Default Focus", true, null) {
override fun getStatMultiplier(stat: Stat) = 1f // actually redundant, but that's two steps to see
},
FoodFocus("${Stat.Food.name} Focus", true, Stat.Food),
ProductionFocus("${Stat.Production.name} Focus", true, Stat.Production),
GoldFocus("${Stat.Gold.name} Focus", true, Stat.Gold),
ScienceFocus("${Stat.Science.name} Focus", true, Stat.Science),
CultureFocus("${Stat.Culture.name} Focus", true, Stat.Culture),
GoldGrowthFocus("Gold Growth Focus", false) {
override fun getStatMultiplier(stat: Stat) = when (stat) {
Stat.Gold, Stat.Food -> 2f
else -> 1f
}
},
ProductionGrowthFocus("Production Growth Focus", false) {
override fun getStatMultiplier(stat: Stat) = when (stat) {
Stat.Production, Stat.Food -> 2f
else -> 1f
}
},
FaithFocus("${Stat.Faith.name} Focus", true, Stat.Faith),
HappinessFocus("${Stat.Happiness.name} Focus", false, Stat.Happiness);
//GreatPersonFocus;
open fun getStatMultiplier(stat: Stat) = when (this.stat) {
stat -> 3f
else -> 1f
}
fun applyWeightTo(stats: Stats) {
for (stat in Stat.values()) {
stats[stat] *= getStatMultiplier(stat)
}
}
fun safeValueOf(stat: Stat): CityFocus {
return values().firstOrNull { it.stat == stat } ?: NoFocus
}
}
class CityInfo { class CityInfo {
@Suppress("JoinDeclarationAndAssignment") @Suppress("JoinDeclarationAndAssignment")
@Transient @Transient
@ -81,10 +125,15 @@ class CityInfo {
/** Tiles that the population in them won't be reassigned */ /** Tiles that the population in them won't be reassigned */
var lockedTiles = HashSet<Vector2>() var lockedTiles = HashSet<Vector2>()
var manualSpecialists = false
var isBeingRazed = false var isBeingRazed = false
var attackedThisTurn = false var attackedThisTurn = false
var hasSoldBuildingThisTurn = false var hasSoldBuildingThisTurn = false
var isPuppet = false var isPuppet = false
var updateCitizens = false // flag so that on endTurn() the Governor reassigns Citizens
var cityAIFocus: CityFocus = CityFocus.NoFocus
var avoidGrowth: Boolean = false
@Transient var currentGPPBonus: Int = 0 // temporary variable saved for rankSpecialist()
/** The very first found city is the _original_ capital, /** The very first found city is the _original_ capital,
* while the _current_ capital can be any other city after the original one is captured. * while the _current_ capital can be any other city after the original one is captured.
@ -148,7 +197,6 @@ class CityInfo {
} }
population.autoAssignPopulation() population.autoAssignPopulation()
cityStats.update()
// Update proximity rankings for all civs // Update proximity rankings for all civs
for (otherCiv in civInfo.gameInfo.getAliveMajorCivs()) { for (otherCiv in civInfo.gameInfo.getAliveMajorCivs()) {
@ -302,6 +350,10 @@ class CityInfo {
toReturn.isOriginalCapital = isOriginalCapital toReturn.isOriginalCapital = isOriginalCapital
toReturn.flagsCountdown.putAll(flagsCountdown) toReturn.flagsCountdown.putAll(flagsCountdown)
toReturn.demandedResource = demandedResource toReturn.demandedResource = demandedResource
toReturn.updateCitizens = updateCitizens
toReturn.cityAIFocus = cityAIFocus
toReturn.avoidGrowth = avoidGrowth
toReturn.manualSpecialists = manualSpecialists
return toReturn return toReturn
} }
@ -436,7 +488,7 @@ class CityInfo {
fun isGrowing() = foodForNextTurn() > 0 fun isGrowing() = foodForNextTurn() > 0
fun isStarving() = foodForNextTurn() < 0 fun isStarving() = foodForNextTurn() < 0
private fun foodForNextTurn() = cityStats.currentCityStats.food.roundToInt() fun foodForNextTurn() = cityStats.currentCityStats.food.roundToInt()
/** Take null to mean infinity. */ /** Take null to mean infinity. */
fun getNumTurnsToNewPopulation(): Int? { fun getNumTurnsToNewPopulation(): Int? {
@ -453,10 +505,32 @@ class CityInfo {
if (!isStarving()) return null if (!isStarving()) return null
return population.foodStored / -foodForNextTurn() + 1 return population.foodStored / -foodForNextTurn() + 1
} }
fun containsBuildingUnique(uniqueType: UniqueType) = fun containsBuildingUnique(uniqueType: UniqueType) =
cityConstructions.getBuiltBuildings().flatMap { it.uniqueObjects }.any { it.isOfType(uniqueType) } cityConstructions.getBuiltBuildings().flatMap { it.uniqueObjects }.any { it.isOfType(uniqueType) }
fun getGreatPersonPercentageBonus(): Int{
var allGppPercentageBonus = 0
for (unique in getMatchingUniques(UniqueType.GreatPersonPointPercentage)
+ getMatchingUniques(UniqueType.GreatPersonPointPercentageDeprecated)
) {
if (!matchesFilter(unique.params[1])) continue
allGppPercentageBonus += unique.params[0].toInt()
}
// Sweden UP
for (otherCiv in civInfo.getKnownCivs()) {
if (!civInfo.getDiplomacyManager(otherCiv).hasFlag(DiplomacyFlags.DeclarationOfFriendship))
continue
for (ourUnique in civInfo.getMatchingUniques(UniqueType.GreatPersonBoostWithFriendship))
allGppPercentageBonus += ourUnique.params[0].toInt()
for (theirUnique in otherCiv.getMatchingUniques(UniqueType.GreatPersonBoostWithFriendship))
allGppPercentageBonus += theirUnique.params[0].toInt()
}
return allGppPercentageBonus
}
fun getGreatPersonPointsForNextTurn(): HashMap<String, Counter<String>> { fun getGreatPersonPointsForNextTurn(): HashMap<String, Counter<String>> {
val sourceToGPP = HashMap<String, Counter<String>>() val sourceToGPP = HashMap<String, Counter<String>>()
@ -481,24 +555,7 @@ class CityInfo {
gppCounter.add(unitName, gppCounter[unitName]!! * unique.params[1].toInt() / 100) gppCounter.add(unitName, gppCounter[unitName]!! * unique.params[1].toInt() / 100)
} }
var allGppPercentageBonus = 0 val allGppPercentageBonus = getGreatPersonPercentageBonus()
for (unique in getMatchingUniques(UniqueType.GreatPersonPointPercentage)
+ getMatchingUniques(UniqueType.GreatPersonPointPercentageDeprecated)
) {
if (!matchesFilter(unique.params[1])) continue
allGppPercentageBonus += unique.params[0].toInt()
}
// Sweden UP
for (otherCiv in civInfo.getKnownCivs()) {
if (!civInfo.getDiplomacyManager(otherCiv).hasFlag(DiplomacyFlags.DeclarationOfFriendship))
continue
for (ourUnique in civInfo.getMatchingUniques(UniqueType.GreatPersonBoostWithFriendship))
allGppPercentageBonus += ourUnique.params[0].toInt()
for (theirUnique in otherCiv.getMatchingUniques(UniqueType.GreatPersonBoostWithFriendship))
allGppPercentageBonus += theirUnique.params[0].toInt()
}
for (unitName in gppCounter.keys) for (unitName in gppCounter.keys)
gppCounter.add(unitName, gppCounter[unitName]!! * allGppPercentageBonus / 100) gppCounter.add(unitName, gppCounter[unitName]!! * allGppPercentageBonus / 100)
@ -585,7 +642,11 @@ class CityInfo {
tryUpdateRoadStatus() tryUpdateRoadStatus()
attackedThisTurn = false attackedThisTurn = false
if (isPuppet) reassignPopulation() if (isPuppet) reassignAllPopulation()
else if (updateCitizens){
reassignPopulation()
updateCitizens = false
}
// The ordering is intentional - you get a turn without WLTKD even if you have the next resource already // The ordering is intentional - you get a turn without WLTKD even if you have the next resource already
if (!hasFlag(CityFlags.WeLoveTheKing)) if (!hasFlag(CityFlags.WeLoveTheKing))
@ -644,19 +705,23 @@ class CityInfo {
demandedResource = "" demandedResource = ""
} }
fun reassignPopulation() { // Reassign all Specialists and Unlock all tiles
var foodWeight = 1f // Mainly for automated cities, Puppets, just captured
var foodPerTurn = 0f fun reassignAllPopulation() {
while (foodWeight < 3 && foodPerTurn <= 0) { manualSpecialists = false
workedTiles = hashSetOf() reassignPopulation(resetLocked = true)
population.specialistAllocations.clear() }
for (i in 0..population.population)
population.autoAssignPopulation(foodWeight)
cityStats.update()
foodPerTurn = foodForNextTurn().toFloat() fun reassignPopulation(resetLocked: Boolean = false) {
foodWeight += 0.5f if (resetLocked) {
workedTiles = hashSetOf()
lockedTiles = hashSetOf()
} else {
workedTiles = lockedTiles
} }
if (!manualSpecialists)
population.specialistAllocations.clear()
population.autoAssignPopulation()
} }
fun endTurn() { fun endTurn() {
@ -905,4 +970,4 @@ class CityInfo {
} }
//endregion //endregion
} }

View File

@ -108,7 +108,7 @@ class CityInfoConquestFunctions(val city: CityInfo){
health = getMaxHealth() / 2 // I think that cities recover to half health when conquered? health = getMaxHealth() / 2 // I think that cities recover to half health when conquered?
if (population.population > 1) population.addPopulation(-1 - population.population / 4) // so from 2-4 population, remove 1, from 5-8, remove 2, etc. if (population.population > 1) population.addPopulation(-1 - population.population / 4) // so from 2-4 population, remove 1, from 5-8, remove 2, etc.
reassignPopulation() reassignAllPopulation()
if (!reconqueredCityWhileStillInResistance && foundingCiv != receivingCiv.civName) { if (!reconqueredCityWhileStillInResistance && foundingCiv != receivingCiv.civName) {
// add resistance // add resistance

View File

@ -2,9 +2,12 @@ package com.unciv.logic.city
import com.unciv.logic.automation.Automation import com.unciv.logic.automation.Automation
import com.unciv.logic.civilization.NotificationIcon import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.map.TileInfo import com.unciv.logic.map.TileInfo
import com.unciv.models.Counter import com.unciv.models.Counter
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stat
import com.unciv.ui.utils.toPercent
import com.unciv.ui.utils.withItem import com.unciv.ui.utils.withItem
import com.unciv.ui.utils.withoutItem import com.unciv.ui.utils.withoutItem
import kotlin.math.floor import kotlin.math.floor
@ -82,7 +85,7 @@ class PopulationManager {
if (percentOfFoodCarriedOver > 95) percentOfFoodCarriedOver = 95 if (percentOfFoodCarriedOver > 95) percentOfFoodCarriedOver = 95
foodStored += (getFoodToNextPopulation() * percentOfFoodCarriedOver / 100f).toInt() foodStored += (getFoodToNextPopulation() * percentOfFoodCarriedOver / 100f).toInt()
addPopulation(1) addPopulation(1)
autoAssignPopulation() cityInfo.updateCitizens = true
cityInfo.civInfo.addNotification("[${cityInfo.name}] has grown!", cityInfo.location, NotificationIcon.Growth) cityInfo.civInfo.addNotification("[${cityInfo.name}] has grown!", cityInfo.location, NotificationIcon.Growth)
} }
} }
@ -109,34 +112,50 @@ class PopulationManager {
addPopulation(-population + count) addPopulation(-population + count)
} }
internal fun autoAssignPopulation(foodWeight: Float = 1f) { internal fun autoAssignPopulation() {
cityInfo.cityStats.update() // calculate current stats with current assignments
val cityStats = cityInfo.cityStats.currentCityStats
cityInfo.currentGPPBonus = cityInfo.getGreatPersonPercentageBonus() // pre-calculate
var specialistFoodBonus = 2f // See CityStats.calcFoodEaten()
for (unique in cityInfo.getMatchingUniques(UniqueType.FoodConsumptionBySpecialists))
if (cityInfo.matchesFilter(unique.params[1]))
specialistFoodBonus *= unique.params[0].toPercent()
specialistFoodBonus = 2f - specialistFoodBonus
for (i in 1..getFreePopulation()) { for (i in 1..getFreePopulation()) {
//evaluate tiles //evaluate tiles
val bestTile: TileInfo? = cityInfo.getTiles() val (bestTile, valueBestTile) = cityInfo.getTiles()
.filter { it.aerialDistanceTo(cityInfo.getCenterTile()) <= 3 } .filter { it.aerialDistanceTo(cityInfo.getCenterTile()) <= 3 }
.filterNot { it.providesYield() } .filterNot { it.providesYield() }
.maxByOrNull { Automation.rankTileForCityWork(it, cityInfo, foodWeight) } .associateWith { Automation.rankTileForCityWork(it, cityInfo, cityStats) }
val valueBestTile = if (bestTile == null) 0f .maxByOrNull { it.value }
else Automation.rankTileForCityWork(bestTile, cityInfo, foodWeight) ?: object : Map.Entry<TileInfo?, Float> {
override val key: TileInfo? = null
override val value = 0f
}
val bestJob: String? = getMaxSpecialists() val bestJob: String? = if (cityInfo.manualSpecialists) null else getMaxSpecialists()
.filter { specialistAllocations[it.key]!! < it.value } .filter { specialistAllocations[it.key]!! < it.value }
.map { it.key } .map { it.key }
.maxByOrNull { Automation.rankSpecialist(getStatsOfSpecialist(it), cityInfo) } .maxByOrNull { Automation.rankSpecialist(it, cityInfo, cityStats) }
var valueBestSpecialist = 0f var valueBestSpecialist = 0f
if (bestJob != null) { if (bestJob != null) {
val specialistStats = getStatsOfSpecialist(bestJob) valueBestSpecialist = Automation.rankSpecialist(bestJob, cityInfo, cityStats)
valueBestSpecialist = Automation.rankSpecialist(specialistStats, cityInfo)
} }
//assign population //assign population
if (valueBestTile > valueBestSpecialist) { if (valueBestTile > valueBestSpecialist) {
if (bestTile != null) if (bestTile != null) {
cityInfo.workedTiles = cityInfo.workedTiles.withItem(bestTile.position) cityInfo.workedTiles = cityInfo.workedTiles.withItem(bestTile.position)
} else if (bestJob != null) specialistAllocations.add(bestJob, 1) cityStats[Stat.Food] += bestTile.getTileStats(cityInfo, cityInfo.civInfo)[Stat.Food]
}
} else if (bestJob != null) {
specialistAllocations.add(bestJob, 1)
cityStats[Stat.Food] += specialistFoodBonus
}
} }
cityInfo.cityStats.update()
} }
fun unassignExtraPopulation() { fun unassignExtraPopulation() {
@ -162,19 +181,19 @@ class PopulationManager {
cityInfo.workedTiles.asSequence() cityInfo.workedTiles.asSequence()
.map { cityInfo.tileMap[it] } .map { cityInfo.tileMap[it] }
.minByOrNull { .minByOrNull {
Automation.rankTileForCityWork(it, cityInfo) Automation.rankTileForCityWork(it, cityInfo, cityInfo.cityStats.currentCityStats)
+(if (it.isLocked()) 10 else 0) +(if (it.isLocked()) 10 else 0)
}!! }!!
} }
val valueWorstTile = if (worstWorkedTile == null) 0f val valueWorstTile = if (worstWorkedTile == null) 0f
else Automation.rankTileForCityWork(worstWorkedTile, cityInfo) else Automation.rankTileForCityWork(worstWorkedTile, cityInfo, cityInfo.cityStats.currentCityStats)
//evaluate specialists //evaluate specialists
val worstJob: String? = specialistAllocations.keys val worstJob: String? = if (cityInfo.manualSpecialists) null else specialistAllocations.keys
.minByOrNull { Automation.rankSpecialist(getStatsOfSpecialist(it), cityInfo) } .minByOrNull { Automation.rankSpecialist(it, cityInfo, cityInfo.cityStats.currentCityStats) }
var valueWorstSpecialist = 0f var valueWorstSpecialist = 0f
if (worstJob != null) if (worstJob != null)
valueWorstSpecialist = Automation.rankSpecialist(getStatsOfSpecialist(worstJob), cityInfo) valueWorstSpecialist = Automation.rankSpecialist(worstJob, cityInfo, cityInfo.cityStats.currentCityStats)
//un-assign population //un-assign population

View File

@ -257,6 +257,9 @@ class TechManager {
UniqueTriggerActivation.triggerCivwideUnique(unique, civInfo) UniqueTriggerActivation.triggerCivwideUnique(unique, civInfo)
} }
updateTransientBooleans() updateTransientBooleans()
for (city in civInfo.cities) {
city.updateCitizens = true
}
civInfo.addNotification("Research of [$techName] has completed!", TechAction(techName), NotificationIcon.Science, techName) civInfo.addNotification("Research of [$techName] has completed!", TechAction(techName), NotificationIcon.Science, techName)
civInfo.popupAlerts.add(PopupAlert(AlertType.TechResearched, techName)) civInfo.popupAlerts.add(PopupAlert(AlertType.TechResearched, techName))

View File

@ -678,6 +678,7 @@ class MapUnit {
} }
tile.improvementInProgress = null tile.improvementInProgress = null
tile.getCity()?.updateCitizens = true
} }

View File

@ -126,6 +126,17 @@ open class Stats(
} }
operator fun div(number: Float) = times(1/number) operator fun div(number: Float) = times(1/number)
/** Apply weighting for Production Ranking */
fun applyRankingWeights(){
food *= 14
production *= 12
gold *= 8 // 3 gold worth about 2 production
science *= 7
culture *= 6
happiness *= 10 // base
faith *= 5
}
/** ***Not*** only a debug helper. It returns a string representing the content, already _translated_. /** ***Not*** only a debug helper. It returns a string representing the content, already _translated_.
* *

View File

@ -0,0 +1,62 @@
package com.unciv.ui.cityscreen
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.logic.city.CityFocus
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.utils.*
class CitizenManagementTable(val cityScreen: CityScreen) : Table() {
private val innerTable = Table()
val city = cityScreen.city
init {
innerTable.background = ImageGetter.getBackground(ImageGetter.getBlue().darken(0.5f))
add(innerTable).pad(2f).fill()
background = ImageGetter.getBackground(Color.WHITE)
}
fun update(visible: Boolean = false) {
innerTable.clear()
if (!visible) {
isVisible = false
return
}
isVisible = true
val colorSelected = BaseScreen.skin.get("selection", Color::class.java)
val colorButton = BaseScreen.skin.get("color", Color::class.java)
// effectively a button, but didn't want to rewrite TextButton style
// and much more compact and can control backgrounds easily based on settings
val avoidLabel = "Avoid Growth".toLabel()
val avoidCell = Table()
avoidCell.touchable = Touchable.enabled
avoidCell.add(avoidLabel).pad(5f)
avoidCell.onClick { city.avoidGrowth = !city.avoidGrowth; city.reassignPopulation(); cityScreen.update() }
avoidCell.background = ImageGetter.getBackground(if (city.avoidGrowth) colorSelected else colorButton)
innerTable.add(avoidCell).colspan(2).growX().pad(3f)
innerTable.row()
for (focus in CityFocus.values()) {
if (!focus.tableEnabled) continue
if (focus == CityFocus.FaithFocus && !city.civInfo.gameInfo.isReligionEnabled()) continue
val label = focus.label.toLabel()
val cell = Table()
cell.touchable = Touchable.enabled
cell.add(label).pad(5f)
cell.onClick { city.cityAIFocus = focus; city.reassignPopulation(); cityScreen.update() }
cell.background = ImageGetter.getBackground(if (city.cityAIFocus == focus) colorSelected else colorButton)
innerTable.add(cell).growX().pad(3f)
if (focus.stat != null)
innerTable.add(ImageGetter.getStatIcon(focus.stat.name)).size(20f).padRight(5f)
innerTable.row()
}
pack()
}
}

View File

@ -15,10 +15,10 @@ import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.tile.TileImprovement import com.unciv.models.ruleset.tile.TileImprovement
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stat import com.unciv.models.stats.Stat
import com.unciv.models.translations.tr
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.map.TileGroupMap import com.unciv.ui.map.TileGroupMap
import com.unciv.ui.popup.ToastPopup import com.unciv.ui.popup.ToastPopup
import com.unciv.ui.tilegroups.TileGroup
import com.unciv.ui.tilegroups.TileSetStrings import com.unciv.ui.tilegroups.TileSetStrings
import com.unciv.ui.utils.* import com.unciv.ui.utils.*
import java.util.* import java.util.*
@ -56,6 +56,12 @@ class CityScreen(
/** Displays raze city button - sits on TOP CENTER */ /** Displays raze city button - sits on TOP CENTER */
private var razeCityButtonHolder = Table() private var razeCityButtonHolder = Table()
/** Displays reset locks button - sits on BOT RIGHT */
private var resetCitizensButtonHolder = Table()
/** Displays reset locks button - sits on BOT RIGHT */
private var citizenManagementButtonHolder = Table()
/** Displays city stats info */ /** Displays city stats info */
private var cityStatsTable = CityStatsTable(this) private var cityStatsTable = CityStatsTable(this)
@ -65,6 +71,10 @@ class CityScreen(
/** Displays selected construction info, alternate with tileTable - sits on BOTTOM RIGHT */ /** Displays selected construction info, alternate with tileTable - sits on BOTTOM RIGHT */
private var selectedConstructionTable = ConstructionInfoTable(this) private var selectedConstructionTable = ConstructionInfoTable(this)
/** Displays selected construction info, alternate with tileTable - sits on BOTTOM RIGHT */
private var citizenManagementTable = CitizenManagementTable(this)
var citizenManagementVisible = false
/** Displays city name, allows switching between cities - sits on BOTTOM CENTER */ /** Displays city name, allows switching between cities - sits on BOTTOM CENTER */
private var cityPickerTable = CityScreenCityPickerTable(this) private var cityPickerTable = CityScreenCityPickerTable(this)
@ -109,10 +119,27 @@ class CityScreen(
//stage.setDebugTableUnderMouse(true) //stage.setDebugTableUnderMouse(true)
stage.addActor(cityStatsTable) stage.addActor(cityStatsTable)
val resetCitizensButton = "Reset Citizens".toTextButton()
resetCitizensButton.labelCell.pad(5f)
resetCitizensButton.onClick { city.reassignPopulation(resetLocked = true); update() }
resetCitizensButtonHolder.add(resetCitizensButton)
resetCitizensButtonHolder.pack()
stage.addActor(resetCitizensButtonHolder)
val citizenManagementButton = "Citizen Management".toTextButton()
citizenManagementButton.labelCell.pad(5f)
citizenManagementButton.onClick {
clearSelection()
citizenManagementVisible = true
update()
}
citizenManagementButtonHolder.add(citizenManagementButton)
citizenManagementButtonHolder.pack()
stage.addActor(citizenManagementButtonHolder)
constructionsTable.addActorsToStage() constructionsTable.addActorsToStage()
stage.addActor(cityInfoTable) stage.addActor(cityInfoTable)
stage.addActor(selectedConstructionTable) stage.addActor(selectedConstructionTable)
stage.addActor(tileTable) stage.addActor(tileTable)
stage.addActor(citizenManagementTable)
stage.addActor(cityPickerTable) // add late so it's top in Z-order and doesn't get covered in cramped portrait stage.addActor(cityPickerTable) // add late so it's top in Z-order and doesn't get covered in cramped portrait
stage.addActor(exitCityButton) stage.addActor(exitCityButton)
update() update()
@ -150,7 +177,24 @@ class CityScreen(
tileTable.setPosition(stage.width - posFromEdge, posFromEdge, Align.bottomRight) tileTable.setPosition(stage.width - posFromEdge, posFromEdge, Align.bottomRight)
selectedConstructionTable.update(selectedConstruction) selectedConstructionTable.update(selectedConstruction)
selectedConstructionTable.setPosition(stage.width - posFromEdge, posFromEdge, Align.bottomRight) selectedConstructionTable.setPosition(stage.width - posFromEdge, posFromEdge, Align.bottomRight)
citizenManagementTable.update(citizenManagementVisible)
citizenManagementTable.setPosition(stage.width - posFromEdge, posFromEdge, Align.bottomRight)
if (selectedTile == null && selectedConstruction == null && !citizenManagementVisible)
resetCitizensButtonHolder.setPosition(stage.width - posFromEdge,
posFromEdge, Align.bottomRight)
else if (selectedConstruction != null)
resetCitizensButtonHolder.setPosition(stage.width - posFromEdge,
posFromEdge + selectedConstructionTable.height + 10f, Align.bottomRight)
else if (selectedTile != null)
resetCitizensButtonHolder.setPosition(stage.width - posFromEdge,
posFromEdge + tileTable.height + 10f, Align.bottomRight)
else
resetCitizensButtonHolder.setPosition(stage.width - posFromEdge,
posFromEdge + citizenManagementTable.height + 10f, Align.bottomRight)
citizenManagementButtonHolder.isVisible = !citizenManagementVisible
citizenManagementButtonHolder.setPosition(stage.width - posFromEdge,
posFromEdge + resetCitizensButtonHolder.y + resetCitizensButtonHolder.height + 10f, Align.bottomRight)
// In portrait mode only: calculate already occupied horizontal space // In portrait mode only: calculate already occupied horizontal space
val rightMargin = when { val rightMargin = when {
!isPortrait() -> 0f !isPortrait() -> 0f
@ -343,9 +387,12 @@ class CityScreen(
if (tileGroup.isWorkable && canChangeState) { if (tileGroup.isWorkable && canChangeState) {
if (!tileInfo.providesYield() && cityInfo.population.getFreePopulation() > 0) { if (!tileInfo.providesYield() && cityInfo.population.getFreePopulation() > 0) {
cityInfo.workedTiles.add(tileInfo.position) cityInfo.workedTiles.add(tileInfo.position)
cityInfo.lockedTiles.add(tileInfo.position)
game.settings.addCompletedTutorialTask("Reassign worked tiles") game.settings.addCompletedTutorialTask("Reassign worked tiles")
} else if (tileInfo.isWorked() && !tileInfo.isLocked()) } else if (tileInfo.isWorked()) {
cityInfo.workedTiles.remove(tileInfo.position) cityInfo.workedTiles.remove(tileInfo.position)
cityInfo.lockedTiles.remove(tileInfo.position)
}
cityInfo.cityStats.update() cityInfo.cityStats.update()
} }
update() update()
@ -365,11 +412,13 @@ class CityScreen(
pickTileData = null pickTileData = null
} }
selectedTile = null selectedTile = null
citizenManagementVisible = false
} }
private fun selectTile(newTile: TileInfo?) { private fun selectTile(newTile: TileInfo?) {
selectedConstruction = null selectedConstruction = null
selectedQueueEntryTargetTile = null selectedQueueEntryTargetTile = null
pickTileData = null pickTileData = null
citizenManagementVisible = false
selectedTile = newTile selectedTile = newTile
} }
fun clearSelection() = selectTile(null) fun clearSelection() = selectTile(null)

View File

@ -7,6 +7,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Align
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.city.CityFlags import com.unciv.logic.city.CityFlags
import com.unciv.logic.city.CityFocus
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stat import com.unciv.models.stats.Stat
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
@ -35,9 +36,24 @@ class CityStatsTable(val cityScreen: CityScreen): Table() {
innerTable.clear() innerTable.clear()
val miniStatsTable = Table() val miniStatsTable = Table()
val selected = BaseScreen.skin.get("selection", Color::class.java)
for ((stat, amount) in cityInfo.cityStats.currentCityStats) { for ((stat, amount) in cityInfo.cityStats.currentCityStats) {
if (stat == Stat.Faith && !cityInfo.civInfo.gameInfo.isReligionEnabled()) continue if (stat == Stat.Faith && !cityInfo.civInfo.gameInfo.isReligionEnabled()) continue
miniStatsTable.add(ImageGetter.getStatIcon(stat.name)).size(20f).padRight(5f) val icon = Table()
if (cityInfo.cityAIFocus.stat == stat) {
icon.add(ImageGetter.getStatIcon(stat.name).surroundWithCircle(27f, false, color = selected))
icon.onClick {
cityInfo.cityAIFocus = CityFocus.NoFocus
cityInfo.reassignPopulation(); cityScreen.update()
}
} else {
icon.add(ImageGetter.getStatIcon(stat.name).surroundWithCircle(27f, false, color = Color.CLEAR))
icon.onClick {
cityInfo.cityAIFocus = cityInfo.cityAIFocus.safeValueOf(stat)
cityInfo.reassignPopulation(); cityScreen.update()
}
}
miniStatsTable.add(icon).size(27f).padRight(5f)
val valueToDisplay = if (stat == Stat.Happiness) cityInfo.cityStats.happinessList.values.sum() else amount val valueToDisplay = if (stat == Stat.Happiness) cityInfo.cityStats.happinessList.values.sum() else amount
miniStatsTable.add(round(valueToDisplay).toInt().toLabel()).padRight(10f) miniStatsTable.add(round(valueToDisplay).toInt().toLabel()).padRight(10f)
} }
@ -57,6 +73,8 @@ class CityStatsTable(val cityScreen: CityScreen): Table() {
private fun addText() { private fun addText() {
val unassignedPopString = "{Unassigned population}: ".tr() + val unassignedPopString = "{Unassigned population}: ".tr() +
cityInfo.population.getFreePopulation().toString() + "/" + cityInfo.population.population cityInfo.population.getFreePopulation().toString() + "/" + cityInfo.population.population
val unassignedPopLabel = unassignedPopString.toLabel()
unassignedPopLabel.onClick { cityInfo.reassignPopulation(); cityScreen.update() }
var turnsToExpansionString = var turnsToExpansionString =
if (cityInfo.cityStats.currentCityStats.culture > 0 && cityInfo.expansion.getChoosableTiles().any()) { if (cityInfo.cityStats.currentCityStats.culture > 0 && cityInfo.expansion.getChoosableTiles().any()) {
@ -80,7 +98,7 @@ class CityStatsTable(val cityScreen: CityScreen): Table() {
}.tr() }.tr()
turnsToPopString += " (${cityInfo.population.foodStored}${Fonts.food}/${cityInfo.population.getFoodToNextPopulation()}${Fonts.food})" turnsToPopString += " (${cityInfo.population.foodStored}${Fonts.food}/${cityInfo.population.getFoodToNextPopulation()}${Fonts.food})"
innerTable.add(unassignedPopString.toLabel()).row() innerTable.add(unassignedPopLabel).row()
innerTable.add(turnsToExpansionString.toLabel()).row() innerTable.add(turnsToExpansionString.toLabel()).row()
innerTable.add(turnsToPopString.toLabel()).row() innerTable.add(turnsToPopString.toLabel()).row()

View File

@ -6,15 +6,27 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Align
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.models.translations.tr
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.utils.* import com.unciv.ui.utils.*
class SpecialistAllocationTable(val cityScreen: CityScreen): Table(BaseScreen.skin){ class SpecialistAllocationTable(val cityScreen: CityScreen) : Table(BaseScreen.skin) {
val cityInfo = cityScreen.city val cityInfo = cityScreen.city
fun update() { fun update() {
clear() clear()
// Auto/Manual Specialists Toggle
// Color of "color" coming from Skin.json that's loaded into BaseScreen
// 5 columns: unassignButton, AllocationTable, assignButton, SeparatorVertical, SpecialistsStatsTabe
if (cityInfo.manualSpecialists) {
val manualSpecialists = "Manual Specialists".toLabel().addBorder(5f, BaseScreen.skin.get("color", Color::class.java))
manualSpecialists.onClick { cityInfo.manualSpecialists = false; cityInfo.reassignPopulation(); cityScreen.update() }
add(manualSpecialists).colspan(5).row()
} else {
val autoSpecialists = "Auto Specialists".toLabel().addBorder(5f, BaseScreen.skin.get("color", Color::class.java))
autoSpecialists.onClick { cityInfo.manualSpecialists = true; update() }
add(autoSpecialists).colspan(5).row()
}
for ((specialistName, maxSpecialists) in cityInfo.population.getMaxSpecialists()) { for ((specialistName, maxSpecialists) in cityInfo.population.getMaxSpecialists()) {
if (!cityInfo.getRuleset().specialists.containsKey(specialistName)) // specialist doesn't exist in this ruleset, probably a mod if (!cityInfo.getRuleset().specialists.containsKey(specialistName)) // specialist doesn't exist in this ruleset, probably a mod
continue continue
@ -31,7 +43,7 @@ class SpecialistAllocationTable(val cityScreen: CityScreen): Table(BaseScreen.sk
} }
fun getAllocationTable(assignedSpecialists: Int, maxSpecialists: Int, specialistName: String):Table{ fun getAllocationTable(assignedSpecialists: Int, maxSpecialists: Int, specialistName: String): Table {
val specialistIconTable = Table() val specialistIconTable = Table()
val specialistObject = cityInfo.getRuleset().specialists[specialistName]!! val specialistObject = cityInfo.getRuleset().specialists[specialistName]!!
@ -44,14 +56,15 @@ class SpecialistAllocationTable(val cityScreen: CityScreen): Table(BaseScreen.sk
return specialistIconTable return specialistIconTable
} }
private fun getAssignButton(assignedSpecialists: Int, maxSpecialists: Int, specialistName: String):Actor { private fun getAssignButton(assignedSpecialists: Int, maxSpecialists: Int, specialistName: String): Actor {
if (assignedSpecialists >= maxSpecialists || cityInfo.isPuppet) return Table() if (assignedSpecialists >= maxSpecialists || cityInfo.isPuppet) return Table()
val assignButton = "+".toLabel(Color.BLACK, Constants.headingFontSize) val assignButton = "+".toLabel(Color.BLACK, Constants.headingFontSize)
.apply { this.setAlignment(Align.center) } .apply { this.setAlignment(Align.center) }
.surroundWithCircle(30f).apply { circle.color= Color.GREEN.darken(0.2f) } .surroundWithCircle(30f).apply { circle.color = Color.GREEN.darken(0.2f) }
assignButton.onClick { assignButton.onClick {
cityInfo.population.specialistAllocations.add(specialistName, 1) cityInfo.population.specialistAllocations.add(specialistName, 1)
cityInfo.manualSpecialists = true
cityInfo.cityStats.update() cityInfo.cityStats.update()
cityScreen.update() cityScreen.update()
} }
@ -60,17 +73,18 @@ class SpecialistAllocationTable(val cityScreen: CityScreen): Table(BaseScreen.sk
return assignButton return assignButton
} }
private fun getUnassignButton(assignedSpecialists: Int, specialistName: String):Actor { private fun getUnassignButton(assignedSpecialists: Int, specialistName: String): Actor {
val unassignButton = "-".toLabel(Color.BLACK,Constants.headingFontSize) val unassignButton = "-".toLabel(Color.BLACK, Constants.headingFontSize)
.apply { this.setAlignment(Align.center) } .apply { this.setAlignment(Align.center) }
.surroundWithCircle(30f).apply { circle.color= Color.RED.darken(0.1f) } .surroundWithCircle(30f).apply { circle.color = Color.RED.darken(0.1f) }
unassignButton.onClick { unassignButton.onClick {
cityInfo.population.specialistAllocations.add(specialistName, -1) cityInfo.population.specialistAllocations.add(specialistName, -1)
cityInfo.manualSpecialists = true
cityInfo.cityStats.update() cityInfo.cityStats.update()
cityScreen.update() cityScreen.update()
} }
if (assignedSpecialists <= 0 || cityInfo.isPuppet) unassignButton.isVisible=false if (assignedSpecialists <= 0 || cityInfo.isPuppet) unassignButton.isVisible = false
if (!UncivGame.Current.worldScreen.isPlayersTurn) unassignButton.clear() if (!UncivGame.Current.worldScreen.isPlayersTurn) unassignButton.clear()
return unassignButton return unassignButton
} }
@ -88,7 +102,7 @@ class SpecialistAllocationTable(val cityScreen: CityScreen): Table(BaseScreen.sk
} }
fun asExpander(onChange: (()->Unit)?): ExpanderTab { fun asExpander(onChange: (() -> Unit)?): ExpanderTab {
return ExpanderTab( return ExpanderTab(
title = "{Specialists}:", title = "{Specialists}:",
fontSize = Constants.defaultFontSize, fontSize = Constants.defaultFontSize,

View File

@ -282,6 +282,7 @@ object UnitActions {
tile.setPillaged() tile.setPillaged()
unit.civInfo.lastSeenImprovement.remove(tile.position) unit.civInfo.lastSeenImprovement.remove(tile.position)
if (tile.resource != null) tile.getOwner()?.updateDetailedCivResources() // this might take away a resource if (tile.resource != null) tile.getOwner()?.updateDetailedCivResources() // this might take away a resource
tile.getCity()?.updateCitizens = true
val freePillage = unit.hasUnique(UniqueType.NoMovementToPillage, checkCivInfoUniques = true) val freePillage = unit.hasUnique(UniqueType.NoMovementToPillage, checkCivInfoUniques = true)
if (!freePillage) unit.useMovementPoints(1f) if (!freePillage) unit.useMovementPoints(1f)