From b61961d0a568ea4a8e4747aa1caa131760c7dd90 Mon Sep 17 00:00:00 2001 From: Oskar Niesen Date: Mon, 10 Jun 2024 14:22:30 -0500 Subject: [PATCH] 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 --- core/src/com/unciv/logic/GameInfo.kt | 2 +- core/src/com/unciv/logic/automation/Automation.kt | 8 ++++---- .../logic/automation/city/ConstructionAutomation.kt | 2 +- .../automation/civilization/NextTurnAutomation.kt | 7 +++---- .../automation/civilization/ReligionAutomation.kt | 2 +- .../unciv/logic/automation/unit/UnitAutomation.kt | 2 +- .../unciv/logic/automation/unit/WorkerAutomation.kt | 2 +- core/src/com/unciv/logic/battle/TargetHelper.kt | 2 +- core/src/com/unciv/logic/city/City.kt | 9 +++++---- .../logic/city/managers/CityExpansionManager.kt | 2 +- .../logic/city/managers/CityPopulationManager.kt | 2 +- .../com/unciv/logic/city/managers/CityTurnManager.kt | 2 +- .../src/com/unciv/logic/civilization/Civilization.kt | 2 ++ .../logic/civilization/managers/ThreatManager.kt | 2 +- core/src/com/unciv/models/ModConstants.kt | 4 ++++ .../com/unciv/ui/screens/cityscreen/CityScreen.kt | 8 +++++--- .../ui/screens/overviewscreen/WonderOverviewTab.kt | 12 ++++++------ .../screens/pickerscreens/ImprovementPickerScreen.kt | 3 ++- .../Mod-file-structure/5-Miscellaneous-JSON-files.md | 6 ++++++ 19 files changed, 47 insertions(+), 32 deletions(-) diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index 49382355cb..4fa03db63c 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -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() } } ) } diff --git a/core/src/com/unciv/logic/automation/Automation.kt b/core/src/com/unciv/logic/automation/Automation.kt index 725d8ea239..abb85b7668 100644 --- a/core/src/com/unciv/logic/automation/Automation.kt +++ b/core/src/com/unciv/logic/automation/Automation.kt @@ -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 } } diff --git a/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt b/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt index c0f05dd2b5..04607b77bb 100644 --- a/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt +++ b/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt @@ -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 diff --git a/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt index 13a3e89ea1..fb6d559f60 100644 --- a/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt @@ -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 diff --git a/core/src/com/unciv/logic/automation/civilization/ReligionAutomation.kt b/core/src/com/unciv/logic/automation/civilization/ReligionAutomation.kt index a674cc1a89..0095b41104 100644 --- a/core/src/com/unciv/logic/automation/civilization/ReligionAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/ReligionAutomation.kt @@ -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 diff --git a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt index 6fbf473a3e..b510b6d3ca 100644 --- a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt @@ -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 } diff --git a/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt b/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt index dd279bc215..b5dae8e7a7 100644 --- a/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt @@ -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) diff --git a/core/src/com/unciv/logic/battle/TargetHelper.kt b/core/src/com/unciv/logic/battle/TargetHelper.kt index 6c3e5d43e3..93b7ea47fd 100644 --- a/core/src/com/unciv/logic/battle/TargetHelper.kt +++ b/core/src/com/unciv/logic/battle/TargetHelper.kt @@ -137,7 +137,7 @@ object TargetHelper { /** Get a list of visible tiles which have something attackable */ fun getBombardableTiles(city: City): Sequence = - city.getCenterTile().getTilesInDistance(city.range) + city.getCenterTile().getTilesInDistance(city.getBombardRange()) .filter { it.isVisible(city.civ) && containsAttackableEnemy(it, CityCombatant(city)) } } diff --git a/core/src/com/unciv/logic/city/City.kt b/core/src/com/unciv/logic/city/City.kt index 7a2e541ae3..90761d471c 100644 --- a/core/src/com/unciv/logic/city/City.kt +++ b/core/src/com/unciv/logic/city/City.kt @@ -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() diff --git a/core/src/com/unciv/logic/city/managers/CityExpansionManager.kt b/core/src/com/unciv/logic/city/managers/CityExpansionManager.kt index 934874a08b..b13e67e894 100644 --- a/core/src/com/unciv/logic/city/managers/CityExpansionManager.kt +++ b/core/src/com/unciv/logic/city/managers/CityExpansionManager.kt @@ -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? { diff --git a/core/src/com/unciv/logic/city/managers/CityPopulationManager.kt b/core/src/com/unciv/logic/city/managers/CityPopulationManager.kt index f387fccb27..40862be196 100644 --- a/core/src/com/unciv/logic/city/managers/CityPopulationManager.kt +++ b/core/src/com/unciv/logic/city/managers/CityPopulationManager.kt @@ -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) } diff --git a/core/src/com/unciv/logic/city/managers/CityTurnManager.kt b/core/src/com/unciv/logic/city/managers/CityTurnManager.kt index a2b7cd8085..ec5f8ffb07 100644 --- a/core/src/com/unciv/logic/city/managers/CityTurnManager.kt +++ b/core/src/com/unciv/logic/city/managers/CityTurnManager.kt @@ -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() diff --git a/core/src/com/unciv/logic/civilization/Civilization.kt b/core/src/com/unciv/logic/civilization/Civilization.kt index 96d883c6a5..ebbac8b24a 100644 --- a/core/src/com/unciv/logic/civilization/Civilization.kt +++ b/core/src/com/unciv/logic/civilization/Civilization.kt @@ -136,6 +136,8 @@ class Civilization : IsPartOfGameInfoSerialization { @Transient var neutralRoads = HashSet() + val modConstants get() = gameInfo.ruleset.modOptions.constants + var playerType = PlayerType.AI /** Used in online multiplayer for human players */ diff --git a/core/src/com/unciv/logic/civilization/managers/ThreatManager.kt b/core/src/com/unciv/logic/civilization/managers/ThreatManager.kt index 68c89d8812..3ac0fbcd1b 100644 --- a/core/src/com/unciv/logic/civilization/managers/ThreatManager.kt +++ b/core/src/com/unciv/logic/civilization/managers/ThreatManager.kt @@ -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 } diff --git a/core/src/com/unciv/models/ModConstants.kt b/core/src/com/unciv/models/ModConstants.kt index d92a1f58fe..c6fa8bfa6a 100644 --- a/core/src/com/unciv/models/ModConstants.kt +++ b/core/src/com/unciv/models/ModConstants.kt @@ -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( diff --git a/core/src/com/unciv/ui/screens/cityscreen/CityScreen.kt b/core/src/com/unciv/ui/screens/cityscreen/CityScreen.kt index 944e02d16e..331ec448d6 100644 --- a/core/src/com/unciv/ui/screens/cityscreen/CityScreen.kt +++ b/core/src/com/unciv/ui/screens/cityscreen/CityScreen.kt @@ -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) } diff --git a/core/src/com/unciv/ui/screens/overviewscreen/WonderOverviewTab.kt b/core/src/com/unciv/ui/screens/overviewscreen/WonderOverviewTab.kt index ba591c3004..99e219163c 100644 --- a/core/src/com/unciv/ui/screens/overviewscreen/WonderOverviewTab.kt +++ b/core/src/com/unciv/ui/screens/overviewscreen/WonderOverviewTab.kt @@ -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 diff --git a/core/src/com/unciv/ui/screens/pickerscreens/ImprovementPickerScreen.kt b/core/src/com/unciv/ui/screens/pickerscreens/ImprovementPickerScreen.kt index 08ac3f2657..6c3edfd71a 100644 --- a/core/src/com/unciv/ui/screens/pickerscreens/ImprovementPickerScreen.kt +++ b/core/src/com/unciv/ui/screens/pickerscreens/ImprovementPickerScreen.kt @@ -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() diff --git a/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md b/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md index c7133caace..8fb7c67d93 100644 --- a/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md +++ b/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md @@ -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)