diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index ee0a4a8fa1..9e8572c097 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -729,6 +729,7 @@ Show worked tiles = Show resources and improvements = Show tile yields = Show unit movement arrows = +Show suggested city locations for units that can found cities = Show pixel units = Show pixel improvements = Unit icon opacity = diff --git a/core/src/com/unciv/logic/automation/unit/CityLocationTileRanker.kt b/core/src/com/unciv/logic/automation/unit/CityLocationTileRanker.kt new file mode 100644 index 0000000000..e05a6a02e7 --- /dev/null +++ b/core/src/com/unciv/logic/automation/unit/CityLocationTileRanker.kt @@ -0,0 +1,145 @@ +package com.unciv.logic.automation.unit + +import com.unciv.logic.automation.Automation +import com.unciv.logic.civilization.Civilization +import com.unciv.logic.civilization.diplomacy.DiplomacyFlags +import com.unciv.logic.map.mapunit.MapUnit +import com.unciv.logic.map.tile.Tile +import com.unciv.models.ruleset.tile.ResourceType +import com.unciv.models.ruleset.tile.TileResource + +class CityLocationTileRanker { + companion object { + fun getBestTilesToFoundCity(unit: MapUnit): Sequence> { + val modConstants = unit.civ.gameInfo.ruleset.modOptions.constants + val tilesNearCities = sequence { + for (city in unit.civ.gameInfo.getCities()) { + val center = city.getCenterTile() + if (unit.civ.knows(city.civ) && + // If the CITY OWNER knows that the UNIT OWNER agreed not to settle near them + city.civ.getDiplomacyManager(unit.civ) + .hasFlag(DiplomacyFlags.AgreedToNotSettleNearUs) + ) { + yieldAll( + center.getTilesInDistance(6) + .filter { canUseTileForRanking(it, unit.civ) }) + continue + } + yieldAll( + center.getTilesInDistance(modConstants.minimalCityDistance) + .filter { canUseTileForRanking(it, unit.civ) } + .filter { it.getContinent() == center.getContinent() } + ) + yieldAll( + center.getTilesInDistance(modConstants.minimalCityDistanceOnDifferentContinents) + .filter { canUseTileForRanking(it, unit.civ) } + .filter { it.getContinent() != center.getContinent() } + ) + } + }.toSet() + + // This is to improve performance - instead of ranking each tile in the area up to 19 times, do it once. + val nearbyTileRankings = getNearbyTileRankings(unit.getTile(), unit.civ) + + val distanceFromHome = if (unit.civ.cities.isEmpty()) 0 + else unit.civ.cities.minOf { it.getCenterTile().aerialDistanceTo(unit.getTile()) } + val range = (8 - distanceFromHome).coerceIn( + 1, + 5 + ) // Restrict vision when far from home to avoid death marches + + val possibleCityLocations = unit.getTile().getTilesInDistance(range) + .filter { canUseTileForRanking(it, unit.civ) } + .filter { + val tileOwner = it.getOwner() + it.isLand && !it.isImpassible() && (tileOwner == null || tileOwner == unit.civ) // don't allow settler to settle inside other civ's territory + && (unit.currentTile == it || unit.movement.canMoveTo(it)) + && it !in tilesNearCities + } + + val luxuryResourcesInCivArea = getLuxuryResourcesInCivArea(unit.civ) + + return possibleCityLocations + .map { + Pair( + it, + rankTileAsCityCenterWithCachedValues( + it, + nearbyTileRankings, + luxuryResourcesInCivArea, + unit.civ + ), + ) + } + .sortedByDescending { it.second } + } + + fun rankTileAsCityCenter(tile: Tile, civ: Civilization): Float { + val nearbyTileRankings = getNearbyTileRankings(tile, civ) + val luxuryResourcesInCivArea = getLuxuryResourcesInCivArea(civ) + return rankTileAsCityCenterWithCachedValues( + tile, + nearbyTileRankings, + luxuryResourcesInCivArea, + civ + ) + } + + private fun canUseTileForRanking( + tile: Tile, + civ: Civilization + ) = + // The AI is allowed to cheat and act like it knows the whole map. + tile.isExplored(civ) || civ.isAI() + + private fun getNearbyTileRankings( + tile: Tile, + civ: Civilization + ): Map { + return tile.getTilesInDistance(7) + .filter { canUseTileForRanking(it, civ) } + .associateBy({ it }, { Automation.rankTile(it, civ) }) + } + + private fun getLuxuryResourcesInCivArea(civ: Civilization): Sequence { + return civ.cities.asSequence() + .flatMap { it.getTiles().asSequence() }.filter { it.resource != null } + .map { it.tileResource }.filter { it.resourceType == ResourceType.Luxury } + .distinct() + } + + private fun rankTileAsCityCenterWithCachedValues( + tile: Tile, nearbyTileRankings: Map, + luxuryResourcesInCivArea: Sequence, + civ: Civilization + ): Float { + val bestTilesFromOuterLayer = tile.getTilesAtDistance(2) + .filter { canUseTileForRanking(it, civ) } + .sortedByDescending { nearbyTileRankings[it] }.take(2) + val top5Tiles = + (tile.neighbors.filter { + canUseTileForRanking( + it, + civ + ) + } + bestTilesFromOuterLayer) + .sortedByDescending { nearbyTileRankings[it] } + .take(5) + var rank = top5Tiles.map { nearbyTileRankings.getValue(it) }.sum() + if (tile.isCoastalTile()) rank += 5 + + val luxuryResourcesInCityArea = + tile.getTilesAtDistance(2).filter { canUseTileForRanking(it, civ) } + .filter { it.resource != null } + .map { it.tileResource }.filter { it.resourceType == ResourceType.Luxury } + .distinct() + val luxuryResourcesAlreadyInCivArea = + luxuryResourcesInCivArea.map { it.name }.toHashSet() + val luxuryResourcesNotYetInCiv = luxuryResourcesInCityArea + .count { it.name !in luxuryResourcesAlreadyInCivArea } + rank += luxuryResourcesNotYetInCiv * 10 + + return rank + } + } +} diff --git a/core/src/com/unciv/logic/automation/unit/SpecificUnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/SpecificUnitAutomation.kt index 97c88275a0..cd24357017 100644 --- a/core/src/com/unciv/logic/automation/unit/SpecificUnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/SpecificUnitAutomation.kt @@ -7,19 +7,14 @@ import com.unciv.logic.battle.GreatGeneralImplementation import com.unciv.logic.battle.MapUnitCombatant import com.unciv.logic.city.City import com.unciv.logic.civilization.Civilization -import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.tile.Tile import com.unciv.models.UnitAction -import com.unciv.models.ruleset.tile.ResourceType -import com.unciv.models.ruleset.tile.TileResource import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.stats.Stat import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsReligion -import kotlin.math.max -import kotlin.math.min object SpecificUnitAutomation { @@ -129,95 +124,36 @@ object SpecificUnitAutomation { .firstOrNull()?.action?.invoke() } - private fun rankTileAsCityCenter(tile: Tile, nearbyTileRankings: Map, - luxuryResourcesInCivArea: Sequence): Float { - val bestTilesFromOuterLayer = tile.getTilesAtDistance(2) - .sortedByDescending { nearbyTileRankings[it] }.take(2) - val top5Tiles = (tile.neighbors + bestTilesFromOuterLayer) - .sortedByDescending { nearbyTileRankings[it] } - .take(5) - var rank = top5Tiles.map { nearbyTileRankings.getValue(it) }.sum() - if (tile.isCoastalTile()) rank += 5 - - val luxuryResourcesInCityArea = tile.getTilesAtDistance(2).filter { it.resource != null } - .map { it.tileResource }.filter { it.resourceType == ResourceType.Luxury }.distinct() - val luxuryResourcesAlreadyInCivArea = luxuryResourcesInCivArea.map { it.name }.toHashSet() - val luxuryResourcesNotYetInCiv = luxuryResourcesInCityArea - .count { it.name !in luxuryResourcesAlreadyInCivArea } - rank += luxuryResourcesNotYetInCiv * 10 - - return rank - } - fun automateSettlerActions(unit: MapUnit) { - val modConstants = unit.civ.gameInfo.ruleset.modOptions.constants - if (unit.getTile().militaryUnit == null // Don't move until you're accompanied by a military unit - && !unit.civ.isCityState() // ..unless you're a city state that was unable to settle its city on turn 1 - && unit.getDamageFromTerrain() < unit.health) return // Also make sure we won't die waiting - - val tilesNearCities = sequence { - for (city in unit.civ.gameInfo.getCities()) { - val center = city.getCenterTile() - if (unit.civ.knows(city.civ) && - // If the CITY OWNER knows that the UNIT OWNER agreed not to settle near them - city.civ.getDiplomacyManager(unit.civ).hasFlag(DiplomacyFlags.AgreedToNotSettleNearUs) - ) { - yieldAll(center.getTilesInDistance(6)) - continue - } - yieldAll(center.getTilesInDistance(modConstants.minimalCityDistance) - .filter { it.getContinent() == center.getContinent() } - ) - yieldAll(center.getTilesInDistance(modConstants.minimalCityDistanceOnDifferentContinents) - .filter { it.getContinent() != center.getContinent() } - ) - } - }.toSet() - - // This is to improve performance - instead of ranking each tile in the area up to 19 times, do it once. - val nearbyTileRankings = unit.getTile().getTilesInDistance(7) - .associateBy({ it }, { Automation.rankTile(it, unit.civ) }) - - val distanceFromHome = if (unit.civ.cities.isEmpty()) 0 - else unit.civ.cities.minOf { it.getCenterTile().aerialDistanceTo(unit.getTile()) } - val range = max(1, min(5, 8 - distanceFromHome)) // Restrict vision when far from home to avoid death marches - - val possibleCityLocations = unit.getTile().getTilesInDistance(range) - .filter { - val tileOwner = it.getOwner() - it.isLand && !it.isImpassible() && (tileOwner == null || tileOwner == unit.civ) // don't allow settler to settle inside other civ's territory - && (unit.currentTile == it || unit.movement.canMoveTo(it)) - && it !in tilesNearCities - }.toList() - - val luxuryResourcesInCivArea = unit.civ.cities.asSequence() - .flatMap { it.getTiles().asSequence() }.filter { it.resource != null } - .map { it.tileResource }.filter { it.resourceType == ResourceType.Luxury } - .distinct() - if (unit.civ.gameInfo.turns == 0) { // Special case, we want AI to settle in place on turn 1. val foundCityAction = UnitActions.getFoundCityAction(unit, unit.getTile()) // Depending on era and difficulty we might start with more than one settler. In that case settle the one with the best location val otherSettlers = unit.civ.units.getCivUnits().filter { it.currentMovement > 0 && it.baseUnit == unit.baseUnit } if(foundCityAction?.action != null && otherSettlers.none { - rankTileAsCityCenter(it.getTile(), nearbyTileRankings, emptySequence()) > rankTileAsCityCenter(unit.getTile(), nearbyTileRankings, emptySequence()) - } ) { + CityLocationTileRanker.rankTileAsCityCenter( + it.getTile(), unit.civ + ) > CityLocationTileRanker.rankTileAsCityCenter( + unit.getTile(), unit.civ + ) + } + ) { foundCityAction.action.invoke() return } } - val citiesByRanking = possibleCityLocations - .map { Pair(it, rankTileAsCityCenter(it, nearbyTileRankings, luxuryResourcesInCivArea)) } - .sortedByDescending { it.second }.toList() + if (unit.getTile().militaryUnit == null // Don't move until you're accompanied by a military unit + && !unit.civ.isCityState() // ..unless you're a city state that was unable to settle its city on turn 1 + && unit.getDamageFromTerrain() < unit.health) return // Also make sure we won't die waiting // It's possible that we'll see a tile "over the sea" that's better than the tiles close by, but that's not a reason to abandon the close tiles! // Also this lead to some routing problems, see https://github.com/yairm210/Unciv/issues/3653 - val bestCityLocation: Tile? = citiesByRanking.firstOrNull { - val pathSize = unit.movement.getShortestPath(it.first).size - return@firstOrNull pathSize in 1..3 - }?.first + val bestCityLocation: Tile? = + CityLocationTileRanker.getBestTilesToFoundCity(unit).firstOrNull { + val pathSize = unit.movement.getShortestPath(it.first).size + return@firstOrNull pathSize in 1..3 + }?.first if (bestCityLocation == null) { // We got a badass over here, all tiles within 5 are taken? // Try to move towards the frontier diff --git a/core/src/com/unciv/models/metadata/GameSettings.kt b/core/src/com/unciv/models/metadata/GameSettings.kt index 7ae26fcca9..e3e3ba261d 100644 --- a/core/src/com/unciv/models/metadata/GameSettings.kt +++ b/core/src/com/unciv/models/metadata/GameSettings.kt @@ -44,6 +44,7 @@ class GameSettings { var showResourcesAndImprovements: Boolean = true var showTileYields: Boolean = false var showUnitMovements: Boolean = false + var showSettlersSuggestedCityLocations: Boolean = true var checkForDueUnits: Boolean = true var autoUnitCycle: Boolean = true diff --git a/core/src/com/unciv/ui/components/tilegroups/TileGroup.kt b/core/src/com/unciv/ui/components/tilegroups/TileGroup.kt index 3b61fac539..7a38479236 100644 --- a/core/src/com/unciv/ui/components/tilegroups/TileGroup.kt +++ b/core/src/com/unciv/ui/components/tilegroups/TileGroup.kt @@ -99,6 +99,7 @@ open class TileGroup( layerMisc.removeHexOutline() layerOverlay.hideHighlight() layerOverlay.hideCrosshair() + layerOverlay.hideGoodCityLocationIndicator() // Show all layers by default setAllLayersVisible(true) diff --git a/core/src/com/unciv/ui/components/tilegroups/layers/TileLayerOverlay.kt b/core/src/com/unciv/ui/components/tilegroups/layers/TileLayerOverlay.kt index af9d89a494..eef3beadce 100644 --- a/core/src/com/unciv/ui/components/tilegroups/layers/TileLayerOverlay.kt +++ b/core/src/com/unciv/ui/components/tilegroups/layers/TileLayerOverlay.kt @@ -14,6 +14,7 @@ class TileLayerOverlay(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup, private val highlight = ImageGetter.getImage(strings().highlight).setHexagonSize() // for blue and red circles/emphasis on the tile private val crosshair = ImageGetter.getImage(strings().crosshair).setHexagonSize() // for when a unit is targeted + private val goodCityLocationIndicator = ImageGetter.getImage("OtherIcons/Cities").setHexagonSize(0.25f) private val fog = ImageGetter.getImage(strings().crosshatchHexagon ).setHexagonSize() private val unexplored = ImageGetter.getImage(strings().unexploredTile ).setHexagonSize() @@ -21,6 +22,7 @@ class TileLayerOverlay(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup, highlight.isVisible = false crosshair.isVisible = false + goodCityLocationIndicator.isVisible = false fog.isVisible = false fog.color = Color.WHITE.cpy().apply { a = 0.2f } @@ -29,6 +31,7 @@ class TileLayerOverlay(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup, addActor(highlight) addActor(fog) addActor(crosshair) + addActor(goodCityLocationIndicator) } fun showCrosshair(alpha: Float = 1f) { @@ -58,10 +61,21 @@ class TileLayerOverlay(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup, determineVisibility() } + fun showGoodCityLocationIndicator() { + goodCityLocationIndicator.isVisible = true + determineVisibility() + } + + fun hideGoodCityLocationIndicator() { + goodCityLocationIndicator.isVisible = false + determineVisibility() + } + fun reset() { fog.isVisible = true highlight.isVisible = false crosshair.isVisible = false + goodCityLocationIndicator.isVisible = false determineVisibility() } @@ -79,7 +93,7 @@ class TileLayerOverlay(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup, } override fun determineVisibility() { - isVisible = fog.isVisible || highlight.isVisible || crosshair.isVisible + isVisible = fog.isVisible || highlight.isVisible || crosshair.isVisible || goodCityLocationIndicator.isVisible } } diff --git a/core/src/com/unciv/ui/popups/options/DisplayTab.kt b/core/src/com/unciv/ui/popups/options/DisplayTab.kt index a9f39803f5..f7969330ea 100644 --- a/core/src/com/unciv/ui/popups/options/DisplayTab.kt +++ b/core/src/com/unciv/ui/popups/options/DisplayTab.kt @@ -76,6 +76,7 @@ fun displayTab( add("Visual Hints".toLabel(fontSize = 24)).colspan(2).row() optionsPopup.addCheckbox(this, "Show unit movement arrows", settings.showUnitMovements, true) { settings.showUnitMovements = it } + optionsPopup.addCheckbox(this, "Show suggested city locations for units that can found cities", settings.showSettlersSuggestedCityLocations, true) { settings.showSettlersSuggestedCityLocations = it } optionsPopup.addCheckbox(this, "Show tile yields", settings.showTileYields, true) { settings.showTileYields = it } // JN optionsPopup.addCheckbox(this, "Show worked tiles", settings.showWorkedTiles, true) { settings.showWorkedTiles = it } optionsPopup.addCheckbox(this, "Show resources and improvements", settings.showResourcesAndImprovements, true) { settings.showResourcesAndImprovements = it } diff --git a/core/src/com/unciv/ui/screens/worldscreen/WorldMapHolder.kt b/core/src/com/unciv/ui/screens/worldscreen/WorldMapHolder.kt index fd8d4525e4..cd864aadf5 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/WorldMapHolder.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/WorldMapHolder.kt @@ -19,6 +19,7 @@ import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.automation.unit.AttackableTile import com.unciv.logic.automation.unit.BattleHelper +import com.unciv.logic.automation.unit.CityLocationTileRanker import com.unciv.logic.automation.unit.UnitAutomation import com.unciv.logic.battle.Battle import com.unciv.logic.battle.MapUnitCombatant @@ -697,6 +698,16 @@ class WorldMapHolder( ) } } + + // Highlight best tiles for city founding + if (unit.hasUnique(UniqueType.FoundCity) + && UncivGame.Current.settings.showSettlersSuggestedCityLocations + ) { + CityLocationTileRanker.getBestTilesToFoundCity(unit).map { it.first } + .filter { it.isExplored(unit.civ) }.take(3).forEach { + tileGroups[it]!!.layerOverlay.showGoodCityLocationIndicator() + } + } } private fun updateBombardableTilesForSelectedCity(city: City) {