diff --git a/android/Images/OtherIcons/Lock.png b/android/Images/OtherIcons/Lock.png index 5bcc0a1e80..f2caf58bf3 100644 Binary files a/android/Images/OtherIcons/Lock.png and b/android/Images/OtherIcons/Lock.png differ diff --git a/android/assets/game.atlas b/android/assets/game.atlas index 167759dc1b..06c5be5a07 100644 --- a/android/assets/game.atlas +++ b/android/assets/game.atlas @@ -1075,6 +1075,90 @@ TileSets/FantasyHex/Arrows/UnitHasAttacked 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/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 rotate: false xy: 4, 830 @@ -1082,6 +1166,118 @@ TileSets/Default/AtollOverlay orig: 100, 100 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/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 rotate: false xy: 1470, 1944 @@ -1089,6 +1285,34 @@ TileSets/Default/CityOverlay orig: 100, 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/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 rotate: false xy: 590, 1836 @@ -1110,6 +1334,20 @@ TileSets/Default/ForestOverlay orig: 100, 100 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/Highlight + rotate: false + xy: 4, 1832 + size: 284, 212 + orig: 284, 212 + offset: 0, 0 + index: -1 TileSets/Default/HillOverlay rotate: false xy: 590, 1728 @@ -1175,263 +1413,25 @@ TileSets/Default/Road index: -1 TileSets/Default/Tiles/River-Bottom rotate: false - xy: 1574, 735 + xy: 1534, 694 size: 32, 28 orig: 32, 28 offset: 0, 0 index: -1 TileSets/Default/Tiles/River-BottomLeft rotate: false - xy: 1574, 699 + xy: 1534, 658 size: 32, 28 orig: 32, 28 offset: 0, 0 index: -1 TileSets/Default/Tiles/River-BottomRight rotate: false - xy: 1574, 663 + xy: 1534, 622 size: 32, 28 orig: 32, 28 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/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 rotate: false xy: 505, 1728 @@ -2316,21 +2316,21 @@ TileSets/FantasyHex/Tiles/Quarry+Stone index: -1 TileSets/FantasyHex/Tiles/River-Bottom rotate: false - xy: 1534, 694 + xy: 1574, 735 size: 32, 28 orig: 32, 28 offset: 0, 0 index: -1 TileSets/FantasyHex/Tiles/River-BottomLeft rotate: false - xy: 1534, 658 + xy: 1574, 699 size: 32, 28 orig: 32, 28 offset: 0, 0 index: -1 TileSets/FantasyHex/Tiles/River-BottomRight rotate: false - xy: 1534, 622 + xy: 1574, 663 size: 32, 28 orig: 32, 28 offset: 0, 0 diff --git a/android/assets/game.png b/android/assets/game.png index 04ff71e6a2..7a6426b97f 100644 Binary files a/android/assets/game.png and b/android/assets/game.png differ diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 08be51cd9e..1766a1a587 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -929,6 +929,8 @@ Nothing = Annex city = Specialist Buildings = Specialist Allocation = +Manual Specialists = +Auto Specialists = Specialists = [specialist] slots = Food eaten = @@ -956,6 +958,16 @@ Worked by [cityName] = Lock = Unlock = 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 select a tile for this building's [improvement] = diff --git a/core/src/com/unciv/logic/automation/Automation.kt b/core/src/com/unciv/logic/automation/Automation.kt index 0420dd03bb..8e3bbf917e 100644 --- a/core/src/com/unciv/logic/automation/Automation.kt +++ b/core/src/com/unciv/logic/automation/Automation.kt @@ -1,5 +1,6 @@ package com.unciv.logic.automation +import com.unciv.logic.city.CityFocus import com.unciv.logic.city.CityInfo import com.unciv.logic.city.INonPerpetualConstruction 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.TileMap import com.unciv.models.ruleset.Building -import com.unciv.models.ruleset.MilestoneType import com.unciv.models.ruleset.Victory import com.unciv.models.ruleset.Victory.Focus 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.UniqueType import com.unciv.models.ruleset.unit.BaseUnit +import com.unciv.models.stats.Stat import com.unciv.models.stats.Stats import com.unciv.ui.victoryscreen.RankingType 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) - return rankStatsForCityWork(stats, city, foodWeight) + return rankStatsForCityWork(stats, city, cityStats) } - private fun rankStatsForCityWork(stats: Stats, city: CityInfo, foodWeight: Float = 1f): Float { - var rank = 0f - if (city.population.population < 5) { - // "small city" - we care more about food and less about global problems like gold science and culture - rank += stats.food * 1.2f * foodWeight - rank += stats.production - rank += stats.science / 2 - rank += stats.culture / 2 - 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 + fun rankSpecialist(specialist: String, cityInfo: CityInfo, cityStats: Stats): Float { + val stats = cityInfo.cityStats.getStatsOfSpecialist(specialist) + var rank = rankStatsForCityWork(stats, cityInfo, cityStats, true) + // derive GPP score + var gpp = 0f + if (cityInfo.getRuleset().specialists.containsKey(specialist)) { // To solve problems in total remake mods + val specialistInfo = cityInfo.getRuleset().specialists[specialist]!! + gpp = specialistInfo.greatPersonPoints.sumValues().toFloat() } + gpp = gpp * (100 + cityInfo.currentGPPBonus) / 100 + rank += gpp * 3 // GPP weight return rank } - internal fun rankSpecialist(stats: Stats, cityInfo: CityInfo): Float { - var rank = rankStatsForCityWork(stats, cityInfo) - rank += 0.3f //GPP bonus - return rank + private fun rankStatsForCityWork(stats: Stats, city: CityInfo, cityStats: Stats, specialist: Boolean = false): Float { + val cityAIFocus = city.cityAIFocus + val yieldStats = stats.clone() + + 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) { @@ -74,8 +122,8 @@ object Automation { mapUnit.getMatchingUniques(UniqueType.CarryAirUnits).firstOrNull() ?: return 0 if (mapUnitCarryUnique.params[1] != carryFilter) return 0 //Carries a different type of unit return mapUnitCarryUnique.params[0].toInt() + - mapUnit.getMatchingUniques(UniqueType.CarryExtraAirUnits) - .filter { it.params[1] == carryFilter }.sumOf { it.params[0].toInt() } + mapUnit.getMatchingUniques(UniqueType.CarryExtraAirUnits) + .filter { it.params[1] == carryFilter }.sumOf { it.params[0].toInt() } } val totalCarriableUnits = @@ -97,13 +145,15 @@ object Automation { findWaterConnectedCitiesAndEnemies.stepToEnd() if (findWaterConnectedCitiesAndEnemies.getReachedTiles().none { (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. militaryUnits = militaryUnits.filter { !it.isWaterUnit() } - val carryingOnlyUnits = militaryUnits.filter { it.hasUnique(UniqueType.CarryAirUnits) - && it.hasUnique(UniqueType.CannotAttack) }.toList() + val carryingOnlyUnits = militaryUnits.filter { + it.hasUnique(UniqueType.CarryAirUnits) + && it.hasUnique(UniqueType.CannotAttack) + }.toList() for (unit in carryingOnlyUnits) if (providesUnneededCarryingSlots(unit, city.civInfo)) @@ -125,9 +175,11 @@ object Automation { .map { it.unitType } .distinct() if (availableTypes.none()) return null - val bestUnitsForType = availableTypes.map { type -> militaryUnits + val bestUnitsForType = availableTypes.map { type -> + militaryUnits .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) val bestForce = bestUnitsForType.maxOf { it.getForceEvaluation() } 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 * and resource scarcity making a construction undesirable. - */ + */ fun allowAutomatedConstruction( civInfo: CivilizationInfo, cityInfo: CityInfo, construction: INonPerpetualConstruction ): Boolean { return allowCreateImprovementBuildings(civInfo, cityInfo, construction) - && allowSpendingResource(civInfo, construction) + && allowSpendingResource(civInfo, construction) } /** 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 // Don't care about old units val neededForUnits = civInfo.lastEraResourceUsedForUnit[resource] != null - && civInfo.lastEraResourceUsedForUnit[resource]!! >= civInfo.getEraNumber() + && civInfo.lastEraResourceUsedForUnit[resource]!! >= civInfo.getEraNumber() // No need to save for both if (!neededForBuilding || !neededForUnits) { @@ -283,11 +335,11 @@ object 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 { it.canBuildImprovement(improvement, cityInfo.civInfo) }.maxByOrNull { - rankTileForCityWork(it, cityInfo) + rankTileForCityWork(it, cityInfo, cityInfo.cityStats.currentCityStats) } } @@ -350,7 +402,7 @@ object Automation { if (adjacentTile.hasViewableResource(cityInfo.civInfo) && (adjacentDistance < 3 || adjacentTile.tileResource.resourceType != ResourceType.Bonus - ) + ) ) score -= 1 if (adjacentTile.naturalWonder != null) { if (adjacentDistance < 3) adjacentNaturalWonder = true @@ -381,7 +433,7 @@ object Automation { } } -enum class ThreatLevel{ +enum class ThreatLevel { VeryLow, Low, Medium, diff --git a/core/src/com/unciv/logic/automation/NextTurnAutomation.kt b/core/src/com/unciv/logic/automation/NextTurnAutomation.kt index 53d70f19a1..daa3207508 100644 --- a/core/src/com/unciv/logic/automation/NextTurnAutomation.kt +++ b/core/src/com/unciv/logic/automation/NextTurnAutomation.kt @@ -853,7 +853,7 @@ object NextTurnAutomation { city.annexCity() } - city.reassignPopulation() + city.reassignAllPopulation() city.cityConstructions.chooseNextConstruction() if (city.health < city.getMaxHealth()) diff --git a/core/src/com/unciv/logic/city/CityInfo.kt b/core/src/com/unciv/logic/city/CityInfo.kt index cc3cc72a7a..77e1759421 100644 --- a/core/src/com/unciv/logic/city/CityInfo.kt +++ b/core/src/com/unciv/logic/city/CityInfo.kt @@ -19,6 +19,7 @@ import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.stats.Stat +import com.unciv.models.stats.Stats import java.util.* import kotlin.collections.ArrayList import kotlin.collections.HashMap @@ -34,6 +35,49 @@ enum class CityFlags { 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 { @Suppress("JoinDeclarationAndAssignment") @Transient @@ -81,10 +125,15 @@ class CityInfo { /** Tiles that the population in them won't be reassigned */ var lockedTiles = HashSet() + var manualSpecialists = false var isBeingRazed = false var attackedThisTurn = false var hasSoldBuildingThisTurn = 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, * while the _current_ capital can be any other city after the original one is captured. @@ -148,7 +197,6 @@ class CityInfo { } population.autoAssignPopulation() - cityStats.update() // Update proximity rankings for all civs for (otherCiv in civInfo.gameInfo.getAliveMajorCivs()) { @@ -302,6 +350,10 @@ class CityInfo { toReturn.isOriginalCapital = isOriginalCapital toReturn.flagsCountdown.putAll(flagsCountdown) toReturn.demandedResource = demandedResource + toReturn.updateCitizens = updateCitizens + toReturn.cityAIFocus = cityAIFocus + toReturn.avoidGrowth = avoidGrowth + toReturn.manualSpecialists = manualSpecialists return toReturn } @@ -436,7 +488,7 @@ class CityInfo { fun isGrowing() = foodForNextTurn() > 0 fun isStarving() = foodForNextTurn() < 0 - private fun foodForNextTurn() = cityStats.currentCityStats.food.roundToInt() + fun foodForNextTurn() = cityStats.currentCityStats.food.roundToInt() /** Take null to mean infinity. */ fun getNumTurnsToNewPopulation(): Int? { @@ -453,10 +505,32 @@ class CityInfo { if (!isStarving()) return null return population.foodStored / -foodForNextTurn() + 1 } - + fun containsBuildingUnique(uniqueType: 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> { val sourceToGPP = HashMap>() @@ -481,24 +555,7 @@ class CityInfo { gppCounter.add(unitName, gppCounter[unitName]!! * unique.params[1].toInt() / 100) } - 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() - } + val allGppPercentageBonus = getGreatPersonPercentageBonus() for (unitName in gppCounter.keys) gppCounter.add(unitName, gppCounter[unitName]!! * allGppPercentageBonus / 100) @@ -585,7 +642,11 @@ class CityInfo { tryUpdateRoadStatus() 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 if (!hasFlag(CityFlags.WeLoveTheKing)) @@ -644,19 +705,23 @@ class CityInfo { demandedResource = "" } - fun reassignPopulation() { - var foodWeight = 1f - var foodPerTurn = 0f - while (foodWeight < 3 && foodPerTurn <= 0) { - workedTiles = hashSetOf() - population.specialistAllocations.clear() - for (i in 0..population.population) - population.autoAssignPopulation(foodWeight) - cityStats.update() + // Reassign all Specialists and Unlock all tiles + // Mainly for automated cities, Puppets, just captured + fun reassignAllPopulation() { + manualSpecialists = false + reassignPopulation(resetLocked = true) + } - foodPerTurn = foodForNextTurn().toFloat() - foodWeight += 0.5f + fun reassignPopulation(resetLocked: Boolean = false) { + if (resetLocked) { + workedTiles = hashSetOf() + lockedTiles = hashSetOf() + } else { + workedTiles = lockedTiles } + if (!manualSpecialists) + population.specialistAllocations.clear() + population.autoAssignPopulation() } fun endTurn() { @@ -905,4 +970,4 @@ class CityInfo { } //endregion -} \ No newline at end of file +} diff --git a/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt b/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt index 1461cd0b0a..2b8e4a1a6c 100644 --- a/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt +++ b/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt @@ -108,7 +108,7 @@ class CityInfoConquestFunctions(val city: CityInfo){ 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. - reassignPopulation() + reassignAllPopulation() if (!reconqueredCityWhileStillInResistance && foundingCiv != receivingCiv.civName) { // add resistance diff --git a/core/src/com/unciv/logic/city/PopulationManager.kt b/core/src/com/unciv/logic/city/PopulationManager.kt index c48eac9573..f5a5a614ab 100644 --- a/core/src/com/unciv/logic/city/PopulationManager.kt +++ b/core/src/com/unciv/logic/city/PopulationManager.kt @@ -2,9 +2,12 @@ package com.unciv.logic.city import com.unciv.logic.automation.Automation import com.unciv.logic.civilization.NotificationIcon +import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.map.TileInfo import com.unciv.models.Counter 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.withoutItem import kotlin.math.floor @@ -82,7 +85,7 @@ class PopulationManager { if (percentOfFoodCarriedOver > 95) percentOfFoodCarriedOver = 95 foodStored += (getFoodToNextPopulation() * percentOfFoodCarriedOver / 100f).toInt() addPopulation(1) - autoAssignPopulation() + cityInfo.updateCitizens = true cityInfo.civInfo.addNotification("[${cityInfo.name}] has grown!", cityInfo.location, NotificationIcon.Growth) } } @@ -109,34 +112,50 @@ class PopulationManager { 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()) { //evaluate tiles - val bestTile: TileInfo? = cityInfo.getTiles() + val (bestTile, valueBestTile) = cityInfo.getTiles() .filter { it.aerialDistanceTo(cityInfo.getCenterTile()) <= 3 } .filterNot { it.providesYield() } - .maxByOrNull { Automation.rankTileForCityWork(it, cityInfo, foodWeight) } - val valueBestTile = if (bestTile == null) 0f - else Automation.rankTileForCityWork(bestTile, cityInfo, foodWeight) + .associateWith { Automation.rankTileForCityWork(it, cityInfo, cityStats) } + .maxByOrNull { it.value } + ?: object : Map.Entry { + 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 } .map { it.key } - .maxByOrNull { Automation.rankSpecialist(getStatsOfSpecialist(it), cityInfo) } - + .maxByOrNull { Automation.rankSpecialist(it, cityInfo, cityStats) } var valueBestSpecialist = 0f if (bestJob != null) { - val specialistStats = getStatsOfSpecialist(bestJob) - valueBestSpecialist = Automation.rankSpecialist(specialistStats, cityInfo) + valueBestSpecialist = Automation.rankSpecialist(bestJob, cityInfo, cityStats) } //assign population if (valueBestTile > valueBestSpecialist) { - if (bestTile != null) + if (bestTile != null) { 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() { @@ -162,19 +181,19 @@ class PopulationManager { cityInfo.workedTiles.asSequence() .map { cityInfo.tileMap[it] } .minByOrNull { - Automation.rankTileForCityWork(it, cityInfo) + Automation.rankTileForCityWork(it, cityInfo, cityInfo.cityStats.currentCityStats) +(if (it.isLocked()) 10 else 0) }!! } val valueWorstTile = if (worstWorkedTile == null) 0f - else Automation.rankTileForCityWork(worstWorkedTile, cityInfo) + else Automation.rankTileForCityWork(worstWorkedTile, cityInfo, cityInfo.cityStats.currentCityStats) //evaluate specialists - val worstJob: String? = specialistAllocations.keys - .minByOrNull { Automation.rankSpecialist(getStatsOfSpecialist(it), cityInfo) } + val worstJob: String? = if (cityInfo.manualSpecialists) null else specialistAllocations.keys + .minByOrNull { Automation.rankSpecialist(it, cityInfo, cityInfo.cityStats.currentCityStats) } var valueWorstSpecialist = 0f if (worstJob != null) - valueWorstSpecialist = Automation.rankSpecialist(getStatsOfSpecialist(worstJob), cityInfo) + valueWorstSpecialist = Automation.rankSpecialist(worstJob, cityInfo, cityInfo.cityStats.currentCityStats) //un-assign population diff --git a/core/src/com/unciv/logic/civilization/TechManager.kt b/core/src/com/unciv/logic/civilization/TechManager.kt index ff8d164865..dce5bfb1cf 100644 --- a/core/src/com/unciv/logic/civilization/TechManager.kt +++ b/core/src/com/unciv/logic/civilization/TechManager.kt @@ -257,6 +257,9 @@ class TechManager { UniqueTriggerActivation.triggerCivwideUnique(unique, civInfo) } updateTransientBooleans() + for (city in civInfo.cities) { + city.updateCitizens = true + } civInfo.addNotification("Research of [$techName] has completed!", TechAction(techName), NotificationIcon.Science, techName) civInfo.popupAlerts.add(PopupAlert(AlertType.TechResearched, techName)) diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index 758362c139..29685df6e8 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -678,6 +678,7 @@ class MapUnit { } tile.improvementInProgress = null + tile.getCity()?.updateCitizens = true } diff --git a/core/src/com/unciv/models/stats/Stats.kt b/core/src/com/unciv/models/stats/Stats.kt index 598ec52034..243b0b628e 100644 --- a/core/src/com/unciv/models/stats/Stats.kt +++ b/core/src/com/unciv/models/stats/Stats.kt @@ -126,6 +126,17 @@ open class Stats( } 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_. * diff --git a/core/src/com/unciv/ui/cityscreen/CitizenManagementTable.kt b/core/src/com/unciv/ui/cityscreen/CitizenManagementTable.kt new file mode 100644 index 0000000000..442e873e4c --- /dev/null +++ b/core/src/com/unciv/ui/cityscreen/CitizenManagementTable.kt @@ -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() + } + +} diff --git a/core/src/com/unciv/ui/cityscreen/CityScreen.kt b/core/src/com/unciv/ui/cityscreen/CityScreen.kt index 1ebf2592f3..bbf2b54773 100644 --- a/core/src/com/unciv/ui/cityscreen/CityScreen.kt +++ b/core/src/com/unciv/ui/cityscreen/CityScreen.kt @@ -15,10 +15,10 @@ import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.tile.TileImprovement import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.stats.Stat +import com.unciv.models.translations.tr import com.unciv.ui.images.ImageGetter import com.unciv.ui.map.TileGroupMap import com.unciv.ui.popup.ToastPopup -import com.unciv.ui.tilegroups.TileGroup import com.unciv.ui.tilegroups.TileSetStrings import com.unciv.ui.utils.* import java.util.* @@ -56,6 +56,12 @@ class CityScreen( /** Displays raze city button - sits on TOP CENTER */ 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 */ private var cityStatsTable = CityStatsTable(this) @@ -65,6 +71,10 @@ class CityScreen( /** Displays selected construction info, alternate with tileTable - sits on BOTTOM RIGHT */ 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 */ private var cityPickerTable = CityScreenCityPickerTable(this) @@ -109,10 +119,27 @@ class CityScreen( //stage.setDebugTableUnderMouse(true) 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() stage.addActor(cityInfoTable) stage.addActor(selectedConstructionTable) 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(exitCityButton) update() @@ -150,7 +177,24 @@ class CityScreen( tileTable.setPosition(stage.width - posFromEdge, posFromEdge, Align.bottomRight) selectedConstructionTable.update(selectedConstruction) 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 val rightMargin = when { !isPortrait() -> 0f @@ -343,9 +387,12 @@ class CityScreen( if (tileGroup.isWorkable && canChangeState) { if (!tileInfo.providesYield() && cityInfo.population.getFreePopulation() > 0) { cityInfo.workedTiles.add(tileInfo.position) + cityInfo.lockedTiles.add(tileInfo.position) game.settings.addCompletedTutorialTask("Reassign worked tiles") - } else if (tileInfo.isWorked() && !tileInfo.isLocked()) + } else if (tileInfo.isWorked()) { cityInfo.workedTiles.remove(tileInfo.position) + cityInfo.lockedTiles.remove(tileInfo.position) + } cityInfo.cityStats.update() } update() @@ -365,11 +412,13 @@ class CityScreen( pickTileData = null } selectedTile = null + citizenManagementVisible = false } private fun selectTile(newTile: TileInfo?) { selectedConstruction = null selectedQueueEntryTargetTile = null pickTileData = null + citizenManagementVisible = false selectedTile = newTile } fun clearSelection() = selectTile(null) diff --git a/core/src/com/unciv/ui/cityscreen/CityStatsTable.kt b/core/src/com/unciv/ui/cityscreen/CityStatsTable.kt index 68b689491c..206b91d996 100644 --- a/core/src/com/unciv/ui/cityscreen/CityStatsTable.kt +++ b/core/src/com/unciv/ui/cityscreen/CityStatsTable.kt @@ -7,6 +7,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.utils.Align import com.unciv.UncivGame import com.unciv.logic.city.CityFlags +import com.unciv.logic.city.CityFocus import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.stats.Stat import com.unciv.models.translations.tr @@ -35,9 +36,24 @@ class CityStatsTable(val cityScreen: CityScreen): Table() { innerTable.clear() val miniStatsTable = Table() + val selected = BaseScreen.skin.get("selection", Color::class.java) for ((stat, amount) in cityInfo.cityStats.currentCityStats) { 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 miniStatsTable.add(round(valueToDisplay).toInt().toLabel()).padRight(10f) } @@ -57,6 +73,8 @@ class CityStatsTable(val cityScreen: CityScreen): Table() { private fun addText() { val unassignedPopString = "{Unassigned population}: ".tr() + cityInfo.population.getFreePopulation().toString() + "/" + cityInfo.population.population + val unassignedPopLabel = unassignedPopString.toLabel() + unassignedPopLabel.onClick { cityInfo.reassignPopulation(); cityScreen.update() } var turnsToExpansionString = if (cityInfo.cityStats.currentCityStats.culture > 0 && cityInfo.expansion.getChoosableTiles().any()) { @@ -80,7 +98,7 @@ class CityStatsTable(val cityScreen: CityScreen): Table() { }.tr() 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(turnsToPopString.toLabel()).row() diff --git a/core/src/com/unciv/ui/cityscreen/SpecialistAllocationTable.kt b/core/src/com/unciv/ui/cityscreen/SpecialistAllocationTable.kt index 2080c574b1..ed86e5c01a 100644 --- a/core/src/com/unciv/ui/cityscreen/SpecialistAllocationTable.kt +++ b/core/src/com/unciv/ui/cityscreen/SpecialistAllocationTable.kt @@ -6,15 +6,27 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.utils.Align import com.unciv.Constants import com.unciv.UncivGame +import com.unciv.models.translations.tr import com.unciv.ui.images.ImageGetter 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 fun update() { 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()) { if (!cityInfo.getRuleset().specialists.containsKey(specialistName)) // specialist doesn't exist in this ruleset, probably a mod 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 specialistObject = cityInfo.getRuleset().specialists[specialistName]!! @@ -44,14 +56,15 @@ class SpecialistAllocationTable(val cityScreen: CityScreen): Table(BaseScreen.sk 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() val assignButton = "+".toLabel(Color.BLACK, Constants.headingFontSize) - .apply { this.setAlignment(Align.center) } - .surroundWithCircle(30f).apply { circle.color= Color.GREEN.darken(0.2f) } + .apply { this.setAlignment(Align.center) } + .surroundWithCircle(30f).apply { circle.color = Color.GREEN.darken(0.2f) } assignButton.onClick { cityInfo.population.specialistAllocations.add(specialistName, 1) + cityInfo.manualSpecialists = true cityInfo.cityStats.update() cityScreen.update() } @@ -60,17 +73,18 @@ class SpecialistAllocationTable(val cityScreen: CityScreen): Table(BaseScreen.sk return assignButton } - private fun getUnassignButton(assignedSpecialists: Int, specialistName: String):Actor { - val unassignButton = "-".toLabel(Color.BLACK,Constants.headingFontSize) - .apply { this.setAlignment(Align.center) } - .surroundWithCircle(30f).apply { circle.color= Color.RED.darken(0.1f) } + private fun getUnassignButton(assignedSpecialists: Int, specialistName: String): Actor { + val unassignButton = "-".toLabel(Color.BLACK, Constants.headingFontSize) + .apply { this.setAlignment(Align.center) } + .surroundWithCircle(30f).apply { circle.color = Color.RED.darken(0.1f) } unassignButton.onClick { cityInfo.population.specialistAllocations.add(specialistName, -1) + cityInfo.manualSpecialists = true cityInfo.cityStats.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() 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( title = "{Specialists}:", fontSize = Constants.defaultFontSize, diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt index 6d4983b5f9..094eaf955c 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt @@ -282,6 +282,7 @@ object UnitActions { tile.setPillaged() unit.civInfo.lastSeenImprovement.remove(tile.position) 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) if (!freePillage) unit.useMovementPoints(1f)