mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-27 05:46:43 -04:00
Show a highlight for the tile that seems most suitable to found a cit… (#9099)
* 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
This commit is contained in:
parent
b0f4e42e99
commit
193114078b
@ -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 =
|
||||
|
@ -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<Pair<Tile, Float>> {
|
||||
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<Tile, Float> {
|
||||
return tile.getTilesInDistance(7)
|
||||
.filter { canUseTileForRanking(it, civ) }
|
||||
.associateBy({ it }, { Automation.rankTile(it, civ) })
|
||||
}
|
||||
|
||||
private fun getLuxuryResourcesInCivArea(civ: Civilization): Sequence<TileResource> {
|
||||
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<Tile, Float>,
|
||||
luxuryResourcesInCivArea: Sequence<TileResource>,
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Tile, Float>,
|
||||
luxuryResourcesInCivArea: Sequence<TileResource>): 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
|
||||
|
@ -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
|
||||
|
@ -99,6 +99,7 @@ open class TileGroup(
|
||||
layerMisc.removeHexOutline()
|
||||
layerOverlay.hideHighlight()
|
||||
layerOverlay.hideCrosshair()
|
||||
layerOverlay.hideGoodCityLocationIndicator()
|
||||
|
||||
// Show all layers by default
|
||||
setAllLayersVisible(true)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 }
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user