Moddable city ranges (#11708)

* Added city bombard, work and expand range ModConstants

* Changed some city range integers to city.getWorkRange() or some equivalent

* Fixed city screen

* Fixed create game error

* Improved support for CityScreen when work range is higher than expand range

* Improved WonderOverviewTab Style

* Improved Civilization.modConstants

* Improved random spacing

* Changed WonderOverviewTab to use a constant again

* Added comments in documentation
This commit is contained in:
Oskar Niesen
2024-06-10 14:22:30 -05:00
committed by GitHub
parent d39c7a97bf
commit b61961d0a5
19 changed files with 47 additions and 32 deletions

View File

@ -463,7 +463,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
thisPlayer,
thisPlayer.cities.filter { city ->
city.canBombard() &&
enemyUnitsCloseToTerritory.any { tile -> tile.aerialDistanceTo(city.getCenterTile()) <= city.range }
enemyUnitsCloseToTerritory.any { tile -> tile.aerialDistanceTo(city.getCenterTile()) <= city.getBombardRange() }
}
)
}

View File

@ -411,12 +411,12 @@ object Automation {
// Resources are good: less points
if (tile.hasViewableResource(city.civ)) {
if (tile.tileResource.resourceType != ResourceType.Bonus) score -= 105
else if (distance <= 3) score -= 104
else if (distance <= city.getWorkRange()) score -= 104
} else {
// Water tiles without resources aren't great
if (tile.isWater) score += 25
// Can't work it anyways
if (distance > 3) score += 100
if (distance > city.getWorkRange()) score += 100
}
if (tile.naturalWonder != null) score -= 105
@ -430,12 +430,12 @@ object Automation {
for (adjacentTile in tile.neighbors.filter { it.getOwner() == null }) {
val adjacentDistance = city.getCenterTile().aerialDistanceTo(adjacentTile)
if (adjacentTile.hasViewableResource(city.civ) &&
(adjacentDistance < 3 ||
(adjacentDistance < city.getWorkRange() ||
adjacentTile.tileResource.resourceType != ResourceType.Bonus
)
) score -= 1
if (adjacentTile.naturalWonder != null) {
if (adjacentDistance < 3) adjacentNaturalWonder = true
if (adjacentDistance < city.getWorkRange()) adjacentNaturalWonder = true
score -= 1
}
}

View File

@ -179,7 +179,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) {
val civilianUnit = city.getCenterTile().civilianUnit
if (civilianUnit != null && civilianUnit.hasUnique(UniqueType.FoundCity)
&& city.getCenterTile().getTilesInDistance(5).none { it.militaryUnit?.civ == civInfo })
&& city.getCenterTile().getTilesInDistance(city.getExpandRange()).none { it.militaryUnit?.civ == civInfo })
modifier = 5f // there's a settler just sitting here, doing nothing - BAD
if (!civInfo.isAIOrAutoPlaying()) modifier /= 2 // Players prefer to make their own unit choices usually

View File

@ -491,10 +491,9 @@ object NextTurnAutomation {
// Otherwise, AI tries to produce settlers when it can hardly sustain itself
.filter { city ->
!workersBuildableForThisCiv
|| city.getCenterTile().getTilesInDistance(2).count { it.improvement!=null } > 1
|| city.getCenterTile().getTilesInDistance(3).any { it.civilianUnit?.hasUnique(UniqueType.BuildImprovements)==true }
}
.maxByOrNull { it.cityStats.currentCityStats.production }
|| city.getCenterTile().getTilesInDistance(civInfo.modConstants.cityWorkRange - 1 ).count { it.improvement != null } > 1
|| city.getCenterTile().getTilesInDistance(civInfo.modConstants.cityWorkRange).any { it.civilianUnit?.hasUnique(UniqueType.BuildImprovements) == true }
}.maxByOrNull { it.cityStats.currentCityStats.production }
?: return
if (bestCity.cityConstructions.getBuiltBuildings().count() > 1) // 2 buildings or more, otherwise focus on self first
bestCity.cityConstructions.currentConstructionFromQueue = settlerUnits.minByOrNull { it.cost }!!.name

View File

@ -201,7 +201,7 @@ object ReligionAutomation {
var score = 0f
for (city in civInfo.cities) {
for (tile in city.getCenterTile().getTilesInDistance(3)) {
for (tile in city.getCenterTile().getTilesInDistance(city.getWorkRange())) {
val tileScore = beliefBonusForTile(belief, tile, city)
score += tileScore * when {
city.workedTiles.contains(tile.position) -> 8

View File

@ -428,7 +428,7 @@ object UnitAutomation {
val tilesWithinBombardmentRange = unit.currentTile.getTilesInDistance(3)
.filter { it.isCityCenter() && it.getCity()!!.civ.isAtWarWith(unit.civ) }
.flatMap { it.getTilesInDistance(it.getCity()!!.range) }
.flatMap { it.getTilesInDistance(it.getCity()!!.getBombardRange()) }
val tilesWithTerrainDamage = unit.currentTile.getTilesInDistance(3)
.filter { unit.getDamageFromTerrain(it) > 0 }

View File

@ -393,7 +393,7 @@ class WorkerAutomation(
// Check if it is not an unowned neighboring tile that can be in city range
&& !(ruleSet.tileImprovements[improvementName]!!.hasUnique(UniqueType.CanBuildOutsideBorders)
&& tile.neighbors.any { it.getOwner() == unit.civ && it.owningCity != null
&& tile.aerialDistanceTo(it.owningCity!!.getCenterTile()) <= 3 } ))
&& tile.aerialDistanceTo(it.owningCity!!.getCenterTile()) <= civInfo.modConstants.cityWorkRange } ))
return 0f
val stats = tile.stats.getStatDiffForImprovement(improvement, civInfo, tile.getCity(), localUniqueCache)

View File

@ -137,7 +137,7 @@ object TargetHelper {
/** Get a list of visible tiles which have something attackable */
fun getBombardableTiles(city: City): Sequence<Tile> =
city.getCenterTile().getTilesInDistance(city.range)
city.getCenterTile().getTilesInDistance(city.getBombardRange())
.filter { it.isVisible(city.civ) && containsAttackableEnemy(it, CityCombatant(city)) }
}

View File

@ -40,9 +40,6 @@ class City : IsPartOfGameInfoSerialization, INamed {
@Transient
private lateinit var centerTile: Tile // cached for better performance
@Transient
val range = 2
@Transient
lateinit var tileMap: TileMap
@ -159,6 +156,10 @@ class City : IsPartOfGameInfoSerialization, INamed {
fun isCapital(): Boolean = cityConstructions.getBuiltBuildings().any { it.hasUnique(UniqueType.IndicatesCapital) }
fun isCoastal(): Boolean = centerTile.isCoastalTile()
fun getBombardRange(): Int = civ.gameInfo.ruleset.modOptions.constants.baseCityBombardRange
fun getWorkRange(): Int = civ.gameInfo.ruleset.modOptions.constants.cityWorkRange
fun getExpandRange(): Int = civ.gameInfo.ruleset.modOptions.constants.cityExpandRange
fun capitalCityIndicator(): Building? {
val indicatorBuildings = getRuleset().buildings.values.asSequence()
.filter { it.hasUnique(UniqueType.IndicatesCapital) }
@ -262,7 +263,7 @@ class City : IsPartOfGameInfoSerialization, INamed {
this.civ = civInfo
tileMap = civInfo.gameInfo.tileMap
centerTile = tileMap[location]
tilesInRange = getCenterTile().getTilesInDistance(3).toHashSet()
tilesInRange = getCenterTile().getTilesInDistance(getWorkRange()).toHashSet()
population.city = this
expansion.city = this
expansion.setTransients()

View File

@ -100,7 +100,7 @@ class CityExpansionManager : IsPartOfGameInfoSerialization {
return cost.roundToInt()
}
fun getChoosableTiles() = city.getCenterTile().getTilesInDistance(5)
fun getChoosableTiles() = city.getCenterTile().getTilesInDistance(city.getExpandRange())
.filter { it.getOwner() == null }
fun chooseNewTileToOwn(): Tile? {

View File

@ -209,7 +209,7 @@ class CityPopulationManager : IsPartOfGameInfoSerialization {
fun unassignExtraPopulation() {
for (tile in city.workedTiles.map { city.tileMap[it] }) {
if (tile.getOwner() != city.civ || tile.getWorkingCity() != city
|| tile.aerialDistanceTo(city.getCenterTile()) > 3)
|| tile.aerialDistanceTo(city.getCenterTile()) > city.getWorkRange())
city.population.stopWorkingTile(tile.position)
}

View File

@ -95,7 +95,7 @@ class CityTurnManager(val city: City) {
it.name != city.demandedResource && // Not same as last time
!city.civ.hasResource(it.name) && // Not one we already have
it.name in city.tileMap.resources && // Must exist somewhere on the map
city.getCenterTile().getTilesInDistance(3).none { nearTile -> nearTile.resource == it.name } // Not in this city's radius
city.getCenterTile().getTilesInDistance(city.getWorkRange()).none { nearTile -> nearTile.resource == it.name } // Not in this city's radius
}
val chosenResource = candidates.randomOrNull()

View File

@ -136,6 +136,8 @@ class Civilization : IsPartOfGameInfoSerialization {
@Transient
var neutralRoads = HashSet<Vector2>()
val modConstants get() = gameInfo.ruleset.modOptions.constants
var playerType = PlayerType.AI
/** Used in online multiplayer for human players */

View File

@ -149,7 +149,7 @@ class ThreatManager(val civInfo: Civilization) {
val tilesWithinBombardmentRange = tilesWithEnemyUnits
.filter { it.isCityCenter() && it.getCity()!!.civ.isAtWarWith(unit.civ) }
.flatMap { it.getTilesInDistance(it.getCity()!!.range) }
.flatMap { it.getTilesInDistance(it.getCity()!!.getBombardRange()) }
val tilesWithTerrainDamage = unit.currentTile.getTilesInDistance(distance)
.filter { unit.getDamageFromTerrain(it) > 0 }

View File

@ -48,6 +48,10 @@ class ModConstants {
var minimalCityDistance = 3
var minimalCityDistanceOnDifferentContinents = 2
var baseCityBombardRange = 2
var cityWorkRange = 3
var cityExpandRange = 5
// Constants used to calculate Unit Upgrade gold Cost (can only be modded all-or-nothing)
// This is a data class for one reason only: The equality implementation enables Gdx Json to omit it when default (otherwise only the individual fields are omitted)
data class UnitUpgradeCost(

View File

@ -44,6 +44,7 @@ import com.unciv.ui.popups.closeAllPopups
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.basescreen.RecreateOnResize
import com.unciv.ui.screens.worldscreen.WorldScreen
import kotlin.math.max
class CityScreen(
internal val city: City,
@ -335,8 +336,9 @@ class CityScreen(
}
private fun addTiles() {
val viewRange = max(city.getExpandRange(), city.getWorkRange())
val tileSetStrings = TileSetStrings()
val cityTileGroups = city.getCenterTile().getTilesInDistance(5)
val cityTileGroups = city.getCenterTile().getTilesInDistance(viewRange)
.filter { selectedCiv.hasExplored(it) }
.map { CityTileGroup(city, it, tileSetStrings, fireworks != null) }
@ -351,8 +353,8 @@ class CityScreen(
for (tileGroup in tileGroups) {
val xDifference = city.getCenterTile().position.x - tileGroup.tile.position.x
val yDifference = city.getCenterTile().position.y - tileGroup.tile.position.y
//if difference is bigger than 5 the tileGroup we are looking for is on the other side of the map
if (xDifference > 5 || xDifference < -5 || yDifference > 5 || yDifference < -5) {
//if difference is bigger than the expansion range the tileGroup we are looking for is on the other side of the map
if (xDifference > viewRange || xDifference < -viewRange || yDifference > viewRange || yDifference < -viewRange) {
//so we want to unwrap its position
tilesToUnwrap.add(tileGroup)
}

View File

@ -244,12 +244,12 @@ class WonderInfo {
}
if (status == WonderStatus.NotFound && !knownFromQuest(viewingPlayer, name)) continue
val city = if (status == WonderStatus.NotFound) null
else tile.getTilesInDistance(5)
.filter { it.isCityCenter() }
.filter { viewingPlayer.knows(it.getOwner()!!) }
.filter { viewingPlayer.hasExplored(it) }
.sortedBy { it.aerialDistanceTo(tile) }
.firstOrNull()?.getCity()
else gameInfo.getCities()
.filter { it.getCenterTile().aerialDistanceTo(tile) <= 5
&& viewingPlayer.knows(it.civ)
&& viewingPlayer.hasExplored(it.getCenterTile()) }
.sortedBy { it.getCenterTile().aerialDistanceTo(tile) }
.firstOrNull()
wonders[index + wonderCount] = WonderInfo(
name, CivilopediaCategories.Terrain,
"Natural Wonders", Color.FOREST, status, civ, city, tile

View File

@ -167,7 +167,8 @@ class ImprovementPickerScreen(
&& !improvement.isRoad()
&& stats.values.any { it > 0f }
&& !improvement.name.startsWith(Constants.remove)
&& !tile.getTilesInDistance(3).any { it.isCityCenter() && it.getCity()!!.civ == currentPlayerCiv }
&& !tile.getTilesInDistance(currentPlayerCiv.modConstants.cityWorkRange)
.any { it.isCityCenter() && it.getCity()!!.civ == currentPlayerCiv }
)
labelText += "\n" + "Not in city work range".tr()

View File

@ -192,6 +192,9 @@ and city distance in another. In case of conflicts, there is no guarantee which
| cityStrengthFromTechsExponent | Float | 2.8 | [^B] |
| cityStrengthFromTechsFullMultiplier | Float | 1.0 | [^B] |
| cityStrengthFromGarrison | Float | 0.2 | [^B] |
| baseCityBombardRange | Int | 2 | [^S] |
| cityWorkRange | Int | 3 | [^T] |
| cityExpandRange | Int | 5 | [^U] |
| unitSupplyPerPopulation | Float | 0.5 | [^C] |
| minimalCityDistance | Int | 3 | [^D] |
| minimalCityDistanceOnDifferentContinents | Int | 2 | [^D] |
@ -225,6 +228,9 @@ Legend:
defensiveBuildingStrength
where %techs is the percentage of techs in the tech tree that are complete
If no techs exist in this ruleset, %techs = 0.5 (=50%)
- [^S]: The distance that cities can attack
- [^T]: The tiles in distance that population in cities can work on. Note: Higher values may lead to performace issues and may cause bugs. cityWorkRange may be greater than cityExpandRange.
- [^U]: The distance that cities can expand their borders to. Note: Higher values may lead to performace issues and may cause bugs.
- [^C]: Formula for Unit Supply:
Supply = unitSupplyBase (difficulties.json)
unitSupplyPerCity \* amountOfCities + (difficulties.json)