From 193114078b0f77857ad2c680449b7e0e32885b54 Mon Sep 17 00:00:00 2001 From: WhoIsJohannes <126110113+WhoIsJohannes@users.noreply.github.com> Date: Thu, 13 Apr 2023 14:43:46 +0200 Subject: [PATCH] =?UTF-8?q?Show=20a=20highlight=20for=20the=20tile=20that?= =?UTF-8?q?=20seems=20most=20suitable=20to=20found=20a=20cit=E2=80=A6=20(#?= =?UTF-8?q?9099)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Show a highlight for the tile that seems most suitable to found a city (can be turned off in settings) * Don't allow players to cheat through highlighting suggested map tiles for city founding. * Don't pass whether cheating is allowed, but just decide based on whether civ is AI or not. That way it will also work correctly for automated settlers (by human players). Also show it in the first round, because why not. If the map generator puts us on a shitty starting tile, why not tell the player? * Address comments * Address comments --- .../jsons/translations/template.properties | 1 + .../automation/unit/CityLocationTileRanker.kt | 145 ++++++++++++++++++ .../automation/unit/SpecificUnitAutomation.kt | 94 ++---------- .../com/unciv/models/metadata/GameSettings.kt | 1 + .../ui/components/tilegroups/TileGroup.kt | 1 + .../tilegroups/layers/TileLayerOverlay.kt | 16 +- .../com/unciv/ui/popups/options/DisplayTab.kt | 1 + .../ui/screens/worldscreen/WorldMapHolder.kt | 11 ++ 8 files changed, 190 insertions(+), 80 deletions(-) create mode 100644 core/src/com/unciv/logic/automation/unit/CityLocationTileRanker.kt 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) {