chore: Split CityInfo functions into CityTurnMAnager and CityFounder

This commit is contained in:
Yair Morgenstern
2023-01-18 21:34:41 +02:00
parent 211a637e31
commit 5ebfcc3a90
12 changed files with 452 additions and 426 deletions

View File

@ -0,0 +1,48 @@
package com.unciv.logic.city
import com.unciv.logic.IsPartOfGameInfoSerialization
import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats
// if tableEnabled == true, then Stat != null
enum class CityFocus(val label: String, val tableEnabled: Boolean, val stat: Stat? = null) :
IsPartOfGameInfoSerialization {
NoFocus("Default Focus", true, null) {
override fun getStatMultiplier(stat: Stat) = 1f // actually redundant, but that's two steps to see
},
FoodFocus("[${Stat.Food.name}] Focus", true, Stat.Food),
ProductionFocus("[${Stat.Production.name}] Focus", true, Stat.Production),
GoldFocus("[${Stat.Gold.name}] Focus", true, Stat.Gold),
ScienceFocus("[${Stat.Science.name}] Focus", true, Stat.Science),
CultureFocus("[${Stat.Culture.name}] Focus", true, Stat.Culture),
GoldGrowthFocus("Gold Growth Focus", false) {
override fun getStatMultiplier(stat: Stat) = when (stat) {
Stat.Gold, Stat.Food -> 2f
else -> 1f
}
},
ProductionGrowthFocus("Production Growth Focus", false) {
override fun getStatMultiplier(stat: Stat) = when (stat) {
Stat.Production, Stat.Food -> 2f
else -> 1f
}
},
FaithFocus("[${Stat.Faith.name}] Focus", true, Stat.Faith),
HappinessFocus("[${Stat.Happiness.name}] Focus", false, Stat.Happiness);
//GreatPersonFocus;
open fun getStatMultiplier(stat: Stat) = when (this.stat) {
stat -> 3f
else -> 1f
}
fun applyWeightTo(stats: Stats) {
for (stat in Stat.values()) {
stats[stat] *= getStatMultiplier(stat)
}
}
fun safeValueOf(stat: Stat): CityFocus {
return values().firstOrNull { it.stat == stat } ?: NoFocus
}
}

View File

@ -9,17 +9,12 @@ import com.unciv.logic.city.managers.CityInfoConquestFunctions
import com.unciv.logic.city.managers.CityPopulationManager
import com.unciv.logic.city.managers.CityReligionManager
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.Proximity
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.managers.ReligionState
import com.unciv.logic.map.RoadStatus
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap
import com.unciv.models.Counter
import com.unciv.models.ruleset.ModOptionsConstants
import com.unciv.models.ruleset.nation.Nation
import com.unciv.models.ruleset.tile.ResourceSupplyList
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.unique.StateForConditionals
@ -27,10 +22,8 @@ import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats
import java.util.*
import kotlin.math.ceil
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.roundToInt
@ -40,48 +33,6 @@ enum class CityFlags {
Resistance
}
// if tableEnabled == true, then Stat != null
enum class CityFocus(val label: String, val tableEnabled: Boolean, val stat: Stat? = null) : IsPartOfGameInfoSerialization {
NoFocus("Default Focus", true, null) {
override fun getStatMultiplier(stat: Stat) = 1f // actually redundant, but that's two steps to see
},
FoodFocus("[${Stat.Food.name}] Focus", true, Stat.Food),
ProductionFocus("[${Stat.Production.name}] Focus", true, Stat.Production),
GoldFocus("[${Stat.Gold.name}] Focus", true, Stat.Gold),
ScienceFocus("[${Stat.Science.name}] Focus", true, Stat.Science),
CultureFocus("[${Stat.Culture.name}] Focus", true, Stat.Culture),
GoldGrowthFocus("Gold Growth Focus", false) {
override fun getStatMultiplier(stat: Stat) = when (stat) {
Stat.Gold, Stat.Food -> 2f
else -> 1f
}
},
ProductionGrowthFocus("Production Growth Focus", false) {
override fun getStatMultiplier(stat: Stat) = when (stat) {
Stat.Production, Stat.Food -> 2f
else -> 1f
}
},
FaithFocus("[${Stat.Faith.name}] Focus", true, Stat.Faith),
HappinessFocus("[${Stat.Happiness.name}] Focus", false, Stat.Happiness);
//GreatPersonFocus;
open fun getStatMultiplier(stat: Stat) = when (this.stat) {
stat -> 3f
else -> 1f
}
fun applyWeightTo(stats: Stats) {
for (stat in Stat.values()) {
stats[stat] *= getStatMultiplier(stat)
}
}
fun safeValueOf(stat: Stat): CityFocus {
return values().firstOrNull { it.stat == stat } ?: NoFocus
}
}
class CityInfo : IsPartOfGameInfoSerialization {
@Suppress("JoinDeclarationAndAssignment")
@ -149,199 +100,10 @@ class CityInfo : IsPartOfGameInfoSerialization {
/** For We Love the King Day */
var demandedResource = ""
private var flagsCountdown = HashMap<String, Int>()
internal var flagsCountdown = HashMap<String, Int>()
fun hasDiplomaticMarriage(): Boolean = foundingCiv == ""
constructor() // for json parsing, we need to have a default constructor
constructor(civInfo: CivilizationInfo, cityLocation: Vector2) { // new city!
this.civInfo = civInfo
foundingCiv = civInfo.civName
turnAcquired = civInfo.gameInfo.turns
location = cityLocation
setTransients()
name = generateNewCityName(
civInfo,
civInfo.gameInfo.civilizations.asSequence().filter { civ -> civ.isAlive() }.toSet(),
arrayListOf("New ", "Neo ", "Nova ", "Altera ")
) ?: "City Without A Name"
isOriginalCapital = civInfo.citiesCreated == 0
if (isOriginalCapital) {
civInfo.hasEverOwnedOriginalCapital = true
// if you have some culture before the 1st city is found, you may want to adopt the 1st policy
civInfo.policies.shouldOpenPolicyPicker = true
}
civInfo.citiesCreated++
civInfo.cities = civInfo.cities.toMutableList().apply { add(this@CityInfo) }
val startingEra = civInfo.gameInfo.gameParameters.startingEra
addStartingBuildings(civInfo, startingEra)
expansion.reset()
tryUpdateRoadStatus()
val tile = getCenterTile()
for (terrainFeature in tile.terrainFeatures.filter {
getRuleset().tileImprovements.containsKey(
"Remove $it"
)
})
tile.removeTerrainFeature(terrainFeature)
tile.changeImprovement(null)
tile.improvementInProgress = null
val ruleset = civInfo.gameInfo.ruleSet
workedTiles = hashSetOf() //reassign 1st working tile
population.setPopulation(ruleset.eras[startingEra]!!.settlerPopulation)
if (civInfo.religionManager.religionState == ReligionState.Pantheon) {
religion.addPressure(
civInfo.religionManager.religion!!.name,
200 * population.population
)
}
population.autoAssignPopulation()
// Update proximity rankings for all civs
for (otherCiv in civInfo.gameInfo.getAliveMajorCivs()) {
if (civInfo.getProximity(otherCiv) != Proximity.Neighbors) // unless already neighbors
civInfo.cache.updateProximity(otherCiv,
otherCiv.cache.updateProximity(civInfo))
}
for (otherCiv in civInfo.gameInfo.getAliveCityStates()) {
if (civInfo.getProximity(otherCiv) != Proximity.Neighbors) // unless already neighbors
civInfo.cache.updateProximity(otherCiv,
otherCiv.cache.updateProximity(civInfo))
}
triggerCitiesSettledNearOtherCiv()
civInfo.gameInfo.cityDistances.setDirty()
}
private fun addStartingBuildings(civInfo: CivilizationInfo, startingEra: String) {
val ruleset = civInfo.gameInfo.ruleSet
if (civInfo.cities.size == 1) cityConstructions.addBuilding(capitalCityIndicator())
// Add buildings and pop we get from starting in this era
for (buildingName in ruleset.eras[startingEra]!!.settlerBuildings) {
val building = ruleset.buildings[buildingName] ?: continue
val uniqueBuilding = civInfo.getEquivalentBuilding(building)
if (uniqueBuilding.isBuildable(cityConstructions))
cityConstructions.addBuilding(uniqueBuilding.name)
}
civInfo.civConstructions.tryAddFreeBuildings()
cityConstructions.addFreeBuildings()
}
/**
* Generates and returns a new city name for the [foundingCiv].
*
* This method attempts to return the first unused city name of the [foundingCiv], taking used
* city names into consideration (including foreign cities). If that fails, it then checks
* whether the civilization has [UniqueType.BorrowsCityNames] and, if true, returns a borrowed
* name. Else, it repeatedly attaches one of the given [prefixes] to the list of names up to ten
* times until an unused name is successfully generated. If all else fails, null is returned.
*
* @param foundingCiv The civilization that founded this city.
* @param aliveCivs Every civilization currently alive.
* @param prefixes Prefixes to add when every base name is taken, ordered.
* @return A new city name in [String]. Null if failed to generate a name.
*/
private fun generateNewCityName(
foundingCiv: CivilizationInfo,
aliveCivs: Set<CivilizationInfo>,
prefixes: List<String>
): String? {
val usedCityNames: Set<String> =
aliveCivs.asSequence().flatMap { civilization ->
civilization.cities.asSequence().map { city -> city.name }
}.toSet()
// Attempt to return the first missing name from the list of city names
for (cityName in foundingCiv.nation.cities) {
if (cityName !in usedCityNames) return cityName
}
// If all names are taken and this nation borrows city names,
// return a random borrowed city name
if (foundingCiv.hasUnique(UniqueType.BorrowsCityNames)) {
return borrowCityName(foundingCiv, aliveCivs, usedCityNames)
}
// If the nation doesn't have the unique above,
// return the first missing name with an increasing number of prefixes attached
// TODO: Make prefixes moddable per nation? Support suffixes?
var candidate: String?
for (number in (1..10)) {
for (prefix in prefixes) {
val currentPrefix: String = prefix.repeat(number)
candidate = foundingCiv.nation.cities.firstOrNull { cityName ->
(currentPrefix + cityName) !in usedCityNames
}
if (candidate != null) return currentPrefix + candidate
}
}
// If all else fails (by using some sort of rule set mod without city names),
return null
}
/**
* Borrows a city name from another major civilization.
*
* @param foundingCiv The civilization that founded this city.
* @param aliveCivs Every civilization currently alive.
* @param usedCityNames Every city name that have already been taken.
* @return A new city named in [String]. Null if failed to generate a name.
*/
private fun borrowCityName(
foundingCiv: CivilizationInfo,
aliveCivs: Set<CivilizationInfo>,
usedCityNames: Set<String>
): String? {
val aliveMajorNations: Sequence<Nation> =
aliveCivs.asSequence().filter { civ -> civ.isMajorCiv() }.map { civ -> civ.nation }
/*
We take the last unused city name for each other major nation in this game,
skipping nations whose names are exhausted,
and choose a random one from that pool if it's not empty.
*/
val otherMajorNations: Sequence<Nation> =
aliveMajorNations.filter { nation -> nation != foundingCiv.nation }
var newCityNames: Set<String> =
otherMajorNations.mapNotNull { nation ->
nation.cities.lastOrNull { city -> city !in usedCityNames }
}.toSet()
if (newCityNames.isNotEmpty()) return newCityNames.random()
// As per fandom wiki, once the names from the other nations in the game are exhausted,
// names are taken from the rest of the major nations in the rule set
val absentMajorNations: Sequence<Nation> =
getRuleset().nations.values.asSequence().filter { nation ->
nation.isMajorCiv() && nation !in aliveMajorNations
}
newCityNames =
absentMajorNations.flatMap { nation ->
nation.cities.asSequence().filter { city -> city !in usedCityNames }
}.toSet()
if (newCityNames.isNotEmpty()) return newCityNames.random()
// If for some reason we have used every single city name in the game,
// (are we using some sort of rule set mod without city names?)
return null
}
//region pure functions
fun clone(): CityInfo {
val toReturn = CityInfo()
@ -634,7 +396,8 @@ class CityInfo : IsPartOfGameInfoSerialization {
//endregion
//region state-changing functions
fun setTransients() {
fun setTransients(civInfo: CivilizationInfo) {
this.civInfo = civInfo
tileMap = civInfo.gameInfo.tileMap
centerTileInfo = tileMap[location]
tilesInRange = getCenterTile().getTilesInDistance(3).toHashSet()
@ -647,65 +410,6 @@ class CityInfo : IsPartOfGameInfoSerialization {
espionage.setTransients(this)
}
fun startTurn() {
// Construct units at the beginning of the turn,
// so they won't be generated out in the open and vulnerable to enemy attacks before you can control them
cityConstructions.constructIfEnough()
cityConstructions.addFreeBuildings()
cityStats.update()
tryUpdateRoadStatus()
attackedThisTurn = false
if (isPuppet) {
cityAIFocus = CityFocus.GoldFocus
reassignAllPopulation()
} else if (updateCitizens) {
reassignPopulation()
updateCitizens = false
}
// The ordering is intentional - you get a turn without WLTKD even if you have the next resource already
if (!hasFlag(CityFlags.WeLoveTheKing))
tryWeLoveTheKing()
nextTurnFlags()
// Seed resource demand countdown
if(demandedResource == "" && !hasFlag(CityFlags.ResourceDemand)) {
setFlag(CityFlags.ResourceDemand,
(if (isCapital()) 25 else 15) + Random().nextInt(10))
}
}
// cf DiplomacyManager nextTurnFlags
private fun nextTurnFlags() {
for (flag in flagsCountdown.keys.toList()) {
if (flagsCountdown[flag]!! > 0)
flagsCountdown[flag] = flagsCountdown[flag]!! - 1
if (flagsCountdown[flag] == 0) {
flagsCountdown.remove(flag)
when (flag) {
CityFlags.ResourceDemand.name -> {
demandNewResource()
}
CityFlags.WeLoveTheKing.name -> {
civInfo.addNotification(
"We Love The King Day in [$name] has ended.",
location, NotificationCategory.General, NotificationIcon.City)
demandNewResource()
}
CityFlags.Resistance.name -> {
civInfo.addNotification(
"The resistance in [$name] has ended!",
location, NotificationCategory.General, "StatIcons/Resistance")
}
}
}
}
}
fun setFlag(flag: CityFlags, amount: Int) {
flagsCountdown[flag.name] = amount
}
@ -741,39 +445,6 @@ class CityInfo : IsPartOfGameInfoSerialization {
population.autoAssignPopulation()
}
fun endTurn() {
val stats = cityStats.currentCityStats
cityConstructions.endTurn(stats)
expansion.nextTurn(stats.culture)
if (isBeingRazed) {
val removedPopulation =
1 + civInfo.getMatchingUniques(UniqueType.CitiesAreRazedXTimesFaster)
.sumOf { it.params[0].toInt() - 1 }
population.addPopulation(-1 * removedPopulation)
if (population.population <= 0) {
civInfo.addNotification(
"[$name] has been razed to the ground!",
location, NotificationCategory.General,
"OtherIcons/Fire"
)
destroyCity()
} else { //if not razed yet:
if (population.foodStored >= population.getFoodToNextPopulation()) { //if surplus in the granary...
population.foodStored =
population.getFoodToNextPopulation() - 1 //...reduce below the new growth threshold
}
}
} else population.nextTurn(foodForNextTurn())
// This should go after the population change, as that might impact the amount of followers in this city
if (civInfo.gameInfo.isReligionEnabled()) religion.endTurn()
if (this in civInfo.cities) { // city was not destroyed
health = min(health + 20, getMaxHealth())
population.unassignExtraPopulation()
}
}
fun destroyCity(overrideSafeties: Boolean = false) {
// Original capitals and holy cities cannot be destroyed,
@ -857,62 +528,6 @@ class CityInfo : IsPartOfGameInfoSerialization {
civInfo.cache.updateCivResources() // this building could be a resource-requiring one
}
private fun demandNewResource() {
val candidates = getRuleset().tileResources.values.filter {
it.resourceType == ResourceType.Luxury && // Must be luxury
!it.hasUnique(UniqueType.CityStateOnlyResource) && // Not a city-state only resource eg jewelry
it.name != demandedResource && // Not same as last time
!civInfo.hasResource(it.name) && // Not one we already have
it.name in tileMap.resources && // Must exist somewhere on the map
getCenterTile().getTilesInDistance(3).none { nearTile -> nearTile.resource == it.name } // Not in this city's radius
}
val chosenResource = candidates.randomOrNull()
/* What if we had a WLTKD before but now the player has every resource in the game? We can't
pick a new resource, so the resource will stay stay the same and the city will demand it
again even if the player still has it. But we shouldn't punish success. */
if (chosenResource != null)
demandedResource = chosenResource.name
if (demandedResource == "") // Failed to get a valid resource, try again some time later
setFlag(CityFlags.ResourceDemand, 15 + Random().nextInt(10))
else
civInfo.addNotification("[$name] demands [$demandedResource]!", location, NotificationCategory.General, NotificationIcon.City, "ResourceIcons/$demandedResource")
}
private fun tryWeLoveTheKing() {
if (demandedResource == "") return
if (civInfo.getCivResourcesByName()[demandedResource]!! > 0) {
setFlag(CityFlags.WeLoveTheKing, 20 + 1) // +1 because it will be decremented by 1 in the same startTurn()
civInfo.addNotification(
"Because they have [$demandedResource], the citizens of [$name] are celebrating We Love The King Day!",
location, NotificationCategory.General, NotificationIcon.City, NotificationIcon.Happiness)
}
}
/*
When someone settles a city within 6 tiles of another civ, this makes the AI unhappy and it starts a rolling event.
The SettledCitiesNearUs flag gets added to the AI so it knows this happened,
and on its turn it asks the player to stop (with a DemandToStopSettlingCitiesNear alert type)
If the player says "whatever, I'm not promising to stop", they get a -10 modifier which gradually disappears in 40 turns
If they DO agree, then if they keep their promise for ~100 turns they get a +10 modifier for keeping the promise,
But if they don't keep their promise they get a -20 that will only fully disappear in 160 turns.
There's a lot of triggering going on here.
*/
private fun triggerCitiesSettledNearOtherCiv() {
val citiesWithin6Tiles =
civInfo.gameInfo.civilizations.asSequence()
.filter { it.isMajorCiv() && it != civInfo }
.flatMap { it.cities }
.filter { it.getCenterTile().aerialDistanceTo(getCenterTile()) <= 6 }
val civsWithCloseCities =
citiesWithin6Tiles
.map { it.civInfo }
.distinct()
.filter { it.knows(civInfo) && it.hasExplored(location) }
for (otherCiv in civsWithCloseCities)
otherCiv.getDiplomacyManager(civInfo).setFlag(DiplomacyFlags.SettledCitiesNearUs, 30)
}
fun canPlaceNewUnit(construction: BaseUnit): Boolean {
val tile = getCenterTile()
return when {

View File

@ -0,0 +1,229 @@
package com.unciv.logic.city.managers
import com.badlogic.gdx.math.Vector2
import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.civilization.Proximity
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.managers.ReligionState
import com.unciv.models.ruleset.nation.Nation
import com.unciv.models.ruleset.unique.UniqueType
class CityFounder {
fun foundCity(civInfo: CivilizationInfo, cityLocation: Vector2) :CityInfo{
val cityInfo = CityInfo()
cityInfo.foundingCiv = civInfo.civName
cityInfo.turnAcquired = civInfo.gameInfo.turns
cityInfo.location = cityLocation
cityInfo.setTransients(civInfo)
cityInfo.name = generateNewCityName(
civInfo,
civInfo.gameInfo.civilizations.asSequence().filter { civ -> civ.isAlive() }.toSet(),
arrayListOf("New ", "Neo ", "Nova ", "Altera ")
) ?: "City Without A Name"
cityInfo.isOriginalCapital = civInfo.citiesCreated == 0
if (cityInfo.isOriginalCapital) {
civInfo.hasEverOwnedOriginalCapital = true
// if you have some culture before the 1st city is found, you may want to adopt the 1st policy
civInfo.policies.shouldOpenPolicyPicker = true
}
civInfo.citiesCreated++
civInfo.cities = civInfo.cities.toMutableList().apply { add(cityInfo) }
val startingEra = civInfo.gameInfo.gameParameters.startingEra
addStartingBuildings(cityInfo, civInfo, startingEra)
cityInfo.expansion.reset()
cityInfo.tryUpdateRoadStatus()
val tile = cityInfo.getCenterTile()
for (terrainFeature in tile.terrainFeatures.filter {
cityInfo.getRuleset().tileImprovements.containsKey(
"Remove $it"
)
})
tile.removeTerrainFeature(terrainFeature)
tile.changeImprovement(null)
tile.improvementInProgress = null
val ruleset = civInfo.gameInfo.ruleSet
cityInfo.workedTiles = hashSetOf() //reassign 1st working tile
cityInfo.population.setPopulation(ruleset.eras[startingEra]!!.settlerPopulation)
if (civInfo.religionManager.religionState == ReligionState.Pantheon) {
cityInfo.religion.addPressure(
civInfo.religionManager.religion!!.name,
200 * cityInfo.population.population
)
}
cityInfo.population.autoAssignPopulation()
// Update proximity rankings for all civs
for (otherCiv in civInfo.gameInfo.getAliveMajorCivs()) {
if (civInfo.getProximity(otherCiv) != Proximity.Neighbors) // unless already neighbors
civInfo.cache.updateProximity(otherCiv,
otherCiv.cache.updateProximity(civInfo))
}
for (otherCiv in civInfo.gameInfo.getAliveCityStates()) {
if (civInfo.getProximity(otherCiv) != Proximity.Neighbors) // unless already neighbors
civInfo.cache.updateProximity(otherCiv,
otherCiv.cache.updateProximity(civInfo))
}
triggerCitiesSettledNearOtherCiv(cityInfo)
civInfo.gameInfo.cityDistances.setDirty()
return cityInfo
}
/**
* Generates and returns a new city name for the [foundingCiv].
*
* This method attempts to return the first unused city name of the [foundingCiv], taking used
* city names into consideration (including foreign cities). If that fails, it then checks
* whether the civilization has [UniqueType.BorrowsCityNames] and, if true, returns a borrowed
* name. Else, it repeatedly attaches one of the given [prefixes] to the list of names up to ten
* times until an unused name is successfully generated. If all else fails, null is returned.
*
* @param foundingCiv The civilization that founded this city.
* @param aliveCivs Every civilization currently alive.
* @param prefixes Prefixes to add when every base name is taken, ordered.
* @return A new city name in [String]. Null if failed to generate a name.
*/
private fun generateNewCityName(
foundingCiv: CivilizationInfo,
aliveCivs: Set<CivilizationInfo>,
prefixes: List<String>
): String? {
val usedCityNames: Set<String> =
aliveCivs.asSequence().flatMap { civilization ->
civilization.cities.asSequence().map { city -> city.name }
}.toSet()
// Attempt to return the first missing name from the list of city names
for (cityName in foundingCiv.nation.cities) {
if (cityName !in usedCityNames) return cityName
}
// If all names are taken and this nation borrows city names,
// return a random borrowed city name
if (foundingCiv.hasUnique(UniqueType.BorrowsCityNames)) {
return borrowCityName(foundingCiv, aliveCivs, usedCityNames)
}
// If the nation doesn't have the unique above,
// return the first missing name with an increasing number of prefixes attached
// TODO: Make prefixes moddable per nation? Support suffixes?
var candidate: String?
for (number in (1..10)) {
for (prefix in prefixes) {
val currentPrefix: String = prefix.repeat(number)
candidate = foundingCiv.nation.cities.firstOrNull { cityName ->
(currentPrefix + cityName) !in usedCityNames
}
if (candidate != null) return currentPrefix + candidate
}
}
// If all else fails (by using some sort of rule set mod without city names),
return null
}
/**
* Borrows a city name from another major civilization.
*
* @param foundingCiv The civilization that founded this city.
* @param aliveCivs Every civilization currently alive.
* @param usedCityNames Every city name that have already been taken.
* @return A new city named in [String]. Null if failed to generate a name.
*/
private fun borrowCityName(
foundingCiv: CivilizationInfo,
aliveCivs: Set<CivilizationInfo>,
usedCityNames: Set<String>
): String? {
val aliveMajorNations: Sequence<Nation> =
aliveCivs.asSequence().filter { civ -> civ.isMajorCiv() }.map { civ -> civ.nation }
/*
We take the last unused city name for each other major nation in this game,
skipping nations whose names are exhausted,
and choose a random one from that pool if it's not empty.
*/
val otherMajorNations: Sequence<Nation> =
aliveMajorNations.filter { nation -> nation != foundingCiv.nation }
var newCityNames: Set<String> =
otherMajorNations.mapNotNull { nation ->
nation.cities.lastOrNull { city -> city !in usedCityNames }
}.toSet()
if (newCityNames.isNotEmpty()) return newCityNames.random()
// As per fandom wiki, once the names from the other nations in the game are exhausted,
// names are taken from the rest of the major nations in the rule set
val absentMajorNations: Sequence<Nation> =
foundingCiv.gameInfo.ruleSet.nations.values.asSequence().filter { nation ->
nation.isMajorCiv() && nation !in aliveMajorNations
}
newCityNames =
absentMajorNations.flatMap { nation ->
nation.cities.asSequence().filter { city -> city !in usedCityNames }
}.toSet()
if (newCityNames.isNotEmpty()) return newCityNames.random()
// If for some reason we have used every single city name in the game,
// (are we using some sort of rule set mod without city names?)
return null
}
private fun addStartingBuildings(cityInfo: CityInfo, civInfo: CivilizationInfo, startingEra: String) {
val ruleset = civInfo.gameInfo.ruleSet
if (civInfo.cities.size == 1) cityInfo.cityConstructions.addBuilding(cityInfo.capitalCityIndicator())
// Add buildings and pop we get from starting in this era
for (buildingName in ruleset.eras[startingEra]!!.settlerBuildings) {
val building = ruleset.buildings[buildingName] ?: continue
val uniqueBuilding = civInfo.getEquivalentBuilding(building)
if (uniqueBuilding.isBuildable(cityInfo.cityConstructions))
cityInfo.cityConstructions.addBuilding(uniqueBuilding.name)
}
civInfo.civConstructions.tryAddFreeBuildings()
cityInfo.cityConstructions.addFreeBuildings()
}
/*
When someone settles a city within 6 tiles of another civ, this makes the AI unhappy and it starts a rolling event.
The SettledCitiesNearUs flag gets added to the AI so it knows this happened,
and on its turn it asks the player to stop (with a DemandToStopSettlingCitiesNear alert type)
If the player says "whatever, I'm not promising to stop", they get a -10 modifier which gradually disappears in 40 turns
If they DO agree, then if they keep their promise for ~100 turns they get a +10 modifier for keeping the promise,
But if they don't keep their promise they get a -20 that will only fully disappear in 160 turns.
There's a lot of triggering going on here.
*/
private fun triggerCitiesSettledNearOtherCiv(cityInfo: CityInfo) {
val citiesWithin6Tiles =
cityInfo.civInfo.gameInfo.civilizations.asSequence()
.filter { it.isMajorCiv() && it != cityInfo.civInfo }
.flatMap { it.cities }
.filter { it.getCenterTile().aerialDistanceTo(cityInfo.getCenterTile()) <= 6 }
val civsWithCloseCities =
citiesWithin6Tiles
.map { it.civInfo }
.distinct()
.filter { it.knows(cityInfo.civInfo) && it.hasExplored(cityInfo.location) }
for (otherCiv in civsWithCloseCities)
otherCiv.getDiplomacyManager(cityInfo.civInfo).setFlag(DiplomacyFlags.SettledCitiesNearUs, 30)
}
}

View File

@ -0,0 +1,147 @@
package com.unciv.logic.city.managers
import com.unciv.logic.city.CityFlags
import com.unciv.logic.city.CityFocus
import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.unique.UniqueType
import java.util.*
import kotlin.math.min
class CityTurnManager(val cityInfo: CityInfo) {
fun startTurn() {
// Construct units at the beginning of the turn,
// so they won't be generated out in the open and vulnerable to enemy attacks before you can control them
cityInfo.cityConstructions.constructIfEnough()
cityInfo.cityConstructions.addFreeBuildings()
cityInfo.cityStats.update()
cityInfo.tryUpdateRoadStatus()
cityInfo.attackedThisTurn = false
if (cityInfo.isPuppet) {
cityInfo.cityAIFocus = CityFocus.GoldFocus
cityInfo.reassignAllPopulation()
} else if (cityInfo.updateCitizens) {
cityInfo.reassignPopulation()
cityInfo.updateCitizens = false
}
// The ordering is intentional - you get a turn without WLTKD even if you have the next resource already
if (!cityInfo.hasFlag(CityFlags.WeLoveTheKing))
tryWeLoveTheKing()
nextTurnFlags()
// Seed resource demand countdown
if (cityInfo.demandedResource == "" && !cityInfo.hasFlag(CityFlags.ResourceDemand)) {
cityInfo.setFlag(
CityFlags.ResourceDemand,
(if (cityInfo.isCapital()) 25 else 15) + Random().nextInt(10))
}
}
private fun tryWeLoveTheKing() {
if (cityInfo.demandedResource == "") return
if (cityInfo.civInfo.getCivResourcesByName()[cityInfo.demandedResource]!! > 0) {
cityInfo.setFlag(CityFlags.WeLoveTheKing, 20 + 1) // +1 because it will be decremented by 1 in the same startTurn()
cityInfo.civInfo.addNotification(
"Because they have [${cityInfo.demandedResource}], the citizens of [${cityInfo.name}] are celebrating We Love The King Day!",
cityInfo.location, NotificationCategory.General, NotificationIcon.City, NotificationIcon.Happiness)
}
}
// cf DiplomacyManager nextTurnFlags
private fun nextTurnFlags() {
for (flag in cityInfo.flagsCountdown.keys.toList()) {
if (cityInfo.flagsCountdown[flag]!! > 0)
cityInfo.flagsCountdown[flag] = cityInfo.flagsCountdown[flag]!! - 1
if (cityInfo.flagsCountdown[flag] == 0) {
cityInfo.flagsCountdown.remove(flag)
when (flag) {
CityFlags.ResourceDemand.name -> {
demandNewResource()
}
CityFlags.WeLoveTheKing.name -> {
cityInfo.civInfo.addNotification(
"We Love The King Day in [${cityInfo.name}] has ended.",
cityInfo.location, NotificationCategory.General, NotificationIcon.City)
demandNewResource()
}
CityFlags.Resistance.name -> {
cityInfo.civInfo.addNotification(
"The resistance in [${cityInfo.name}] has ended!",
cityInfo.location, NotificationCategory.General, "StatIcons/Resistance")
}
}
}
}
}
private fun demandNewResource() {
val candidates = cityInfo.getRuleset().tileResources.values.filter {
it.resourceType == ResourceType.Luxury && // Must be luxury
!it.hasUnique(UniqueType.CityStateOnlyResource) && // Not a city-state only resource eg jewelry
it.name != cityInfo.demandedResource && // Not same as last time
!cityInfo.civInfo.hasResource(it.name) && // Not one we already have
it.name in cityInfo.tileMap.resources && // Must exist somewhere on the map
cityInfo.getCenterTile().getTilesInDistance(3).none { nearTile -> nearTile.resource == it.name } // Not in this city's radius
}
val chosenResource = candidates.randomOrNull()
/* What if we had a WLTKD before but now the player has every resource in the game? We can't
pick a new resource, so the resource will stay stay the same and the city will demand it
again even if the player still has it. But we shouldn't punish success. */
if (chosenResource != null)
cityInfo.demandedResource = chosenResource.name
if (cityInfo.demandedResource == "") // Failed to get a valid resource, try again some time later
cityInfo.setFlag(CityFlags.ResourceDemand, 15 + Random().nextInt(10))
else
cityInfo.civInfo.addNotification("[${cityInfo.name}] demands [${cityInfo.demandedResource}]!",
cityInfo.location, NotificationCategory.General, NotificationIcon.City, "ResourceIcons/${cityInfo.demandedResource}")
}
fun endTurn() {
val stats = cityInfo.cityStats.currentCityStats
cityInfo.cityConstructions.endTurn(stats)
cityInfo.expansion.nextTurn(stats.culture)
if (cityInfo.isBeingRazed) {
val removedPopulation =
1 + cityInfo.civInfo.getMatchingUniques(UniqueType.CitiesAreRazedXTimesFaster)
.sumOf { it.params[0].toInt() - 1 }
cityInfo.population.addPopulation(-1 * removedPopulation)
if (cityInfo.population.population <= 0) {
cityInfo.civInfo.addNotification(
"[${cityInfo.name}] has been razed to the ground!",
cityInfo.location, NotificationCategory.General,
"OtherIcons/Fire"
)
cityInfo.destroyCity()
} else { //if not razed yet:
if (cityInfo.population.foodStored >= cityInfo.population.getFoodToNextPopulation()) { //if surplus in the granary...
cityInfo.population.foodStored =
cityInfo.population.getFoodToNextPopulation() - 1 //...reduce below the new growth threshold
}
}
} else cityInfo.population.nextTurn(cityInfo.foodForNextTurn())
// This should go after the population change, as that might impact the amount of followers in this city
if (cityInfo.civInfo.gameInfo.isReligionEnabled()) cityInfo.religion.endTurn()
if (cityInfo in cityInfo.civInfo.cities) { // city was not destroyed
cityInfo.health = min(cityInfo.health + 20, cityInfo.getMaxHealth())
cityInfo.population.unassignExtraPopulation()
}
}
}

View File

@ -10,6 +10,7 @@ import com.unciv.logic.UncivShowableException
import com.unciv.logic.automation.ai.TacticalAI
import com.unciv.logic.automation.unit.WorkerAutomation
import com.unciv.logic.city.CityInfo
import com.unciv.logic.city.managers.CityFounder
import com.unciv.logic.civilization.diplomacy.CityStateFunctions
import com.unciv.logic.civilization.diplomacy.CityStatePersonality
import com.unciv.logic.civilization.diplomacy.DiplomacyFunctions
@ -625,40 +626,22 @@ class CivilizationInfo : IsPartOfGameInfoSerialization {
fun setTransients() {
goldenAges.civInfo = this
greatPeople.civInfo = this
civConstructions.setTransients(civInfo = this)
policies.civInfo = this
if (policies.adoptedPolicies.size > 0 && policies.numberOfAdoptedPolicies == 0)
policies.numberOfAdoptedPolicies = policies.adoptedPolicies.count { !Policy.isBranchCompleteByName(it) }
policies.setTransients()
questManager.civInfo = this
questManager.setTransients()
if (citiesCreated == 0 && cities.any())
citiesCreated = cities.filter { it.name in nation.cities }.size
religionManager.civInfo = this // needs to be before tech, since tech setTransients looks at all uniques
religionManager.setTransients()
tech.civInfo = this
tech.setTransients()
policies.setTransients(this)
questManager.setTransients(this)
religionManager.setTransients(this) // needs to be before tech, since tech setTransients looks at all uniques
tech.setTransients(this)
ruinsManager.setTransients(this)
espionageManager.setTransients(this)
victoryManager.civInfo = this
for (diplomacyManager in diplomacy.values) {
diplomacyManager.civInfo = this
diplomacyManager.updateHasOpenBorders()
}
espionageManager.setTransients(this)
victoryManager.civInfo = this
for (cityInfo in cities) {
cityInfo.civInfo = this // must be before the city's setTransients because it depends on the tilemap, that comes from the currentPlayerCivInfo
cityInfo.setTransients()
cityInfo.setTransients(this) // must be before the city's setTransients because it depends on the tilemap, that comes from the currentPlayerCivInfo
}
// Now that all tile transients have been updated, clean "worked" tiles that are not under the Civ's control
@ -742,7 +725,6 @@ class CivilizationInfo : IsPartOfGameInfoSerialization {
}
}
fun addNotification(text: String, location: Vector2, category:NotificationCategory, vararg notificationIcons: String) {
addNotification(text, LocationAction(location), category, *notificationIcons)
}
@ -757,7 +739,7 @@ class CivilizationInfo : IsPartOfGameInfoSerialization {
}
fun addCity(location: Vector2) {
val newCity = CityInfo(this, location)
val newCity = CityFounder().foundCity(this, location)
newCity.cityConstructions.chooseNextConstruction()
}

View File

@ -97,7 +97,8 @@ class PolicyManager : IsPartOfGameInfoSerialization {
@Suppress("MemberVisibilityCanBePrivate")
fun getPolicyByName(name: String): Policy = getRulesetPolicies()[name]!!
fun setTransients() {
fun setTransients(civInfo: CivilizationInfo) {
this.civInfo = civInfo
for (policyName in adoptedPolicies) addPolicyToTransients(
getPolicyByName(policyName)
)

View File

@ -100,7 +100,8 @@ class QuestManager : IsPartOfGameInfoSerialization {
return toReturn
}
fun setTransients() {
fun setTransients(civInfo: CivilizationInfo) {
this.civInfo = civInfo
for (quest in assignedQuests)
quest.gameInfo = civInfo.gameInfo
}

View File

@ -60,7 +60,8 @@ class ReligionManager : IsPartOfGameInfoSerialization {
return clone
}
fun setTransients() {
fun setTransients(civInfo: CivilizationInfo) {
this.civInfo = civInfo
// Find our religion from the map of founded religions.
// First check if there is any major religion
religion = civInfo.gameInfo.religions.values.firstOrNull {

View File

@ -418,7 +418,8 @@ class TechManager : IsPartOfGameInfoSerialization {
techUniques.addUniques(tech.uniqueObjects)
}
fun setTransients() {
fun setTransients(civInfo: CivilizationInfo) {
this.civInfo = civInfo
researchedTechnologies.addAll(techsResearched.map { getRuleset().technologies[it]!! })
researchedTechnologies.forEach { addTechToTransients(it) }
updateEra() // before updateTransientBooleans so era-based conditionals can work

View File

@ -3,6 +3,7 @@ package com.unciv.logic.civilization.managers
import com.unciv.UncivGame
import com.unciv.logic.VictoryData
import com.unciv.logic.automation.civilization.NextTurnAutomation
import com.unciv.logic.city.managers.CityTurnManager
import com.unciv.logic.civilization.AlertType
import com.unciv.logic.civilization.CivFlags
import com.unciv.logic.civilization.CivilizationInfo
@ -48,7 +49,7 @@ class TurnManager(val civInfo: CivilizationInfo) {
civInfo.cache.updateCitiesConnectedToCapital()
startTurnFlags()
updateRevolts()
for (city in civInfo.cities) city.startTurn() // Most expensive part of startTurn
for (city in civInfo.cities) CityTurnManager(city).startTurn() // Most expensive part of startTurn
for (unit in civInfo.units.getCivUnits()) unit.startTurn()
@ -257,7 +258,7 @@ class TurnManager(val civInfo: CivilizationInfo) {
yieldAll(civInfo.cities.filter { it.isBeingRazed })
yieldAll(civInfo.cities.filterNot { it.isBeingRazed })
}.toList()) { // a city can be removed while iterating (if it's being razed) so we need to iterate over a copy
city.endTurn()
CityTurnManager(city).endTurn()
}
civInfo.temporaryUniques.endTurn()

View File

@ -9,9 +9,9 @@ import com.unciv.logic.civilization.transients.CapitalConnectionsFinder
import com.unciv.logic.map.RoadStatus
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap
import com.unciv.models.ruleset.nation.Nation
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.nation.Nation
import com.unciv.models.ruleset.tile.TerrainType
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.testing.GdxTestRunner
@ -113,14 +113,13 @@ class CapitalConnectionsFinderTests {
private fun createCity(civInfo: CivilizationInfo, position: Vector2, name: String, capital: Boolean = false, hasHarbor: Boolean = false): CityInfo {
return CityInfo().apply {
this.civInfo = civInfo
location = position
if (capital)
cityConstructions.builtBuildings.add(rules.buildings.values.first { it.hasUnique(UniqueType.IndicatesCapital) }.name)
if (hasHarbor)
cityConstructions.builtBuildings.add(rules.buildings.values.first { it.hasUnique(UniqueType.ConnectTradeRoutes) }.name)
this.name = name
setTransients()
setTransients(civInfo)
tilesMap[location].setOwningCity(this)
}
}

View File

@ -5,6 +5,7 @@ import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.GameInfo
import com.unciv.logic.city.CityInfo
import com.unciv.logic.city.managers.CityFounder
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.civilization.PlayerType
import com.unciv.logic.map.MapSizeNew
@ -18,12 +19,12 @@ import com.unciv.models.ruleset.Belief
import com.unciv.models.ruleset.BeliefType
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.IRulesetObject
import com.unciv.models.ruleset.nation.Nation
import com.unciv.models.ruleset.Policy
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.Specialist
import com.unciv.models.ruleset.Speed
import com.unciv.models.ruleset.nation.Nation
import com.unciv.models.ruleset.tile.TileImprovement
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit
@ -133,7 +134,7 @@ class TestGame {
replacePalace: Boolean = false,
initialPopulation: Int = 0
): CityInfo {
val cityInfo = CityInfo(civInfo, tile.position)
val cityInfo = CityFounder().foundCity(civInfo, tile.position)
if (initialPopulation != 1)
cityInfo.population.addPopulation(initialPopulation - 1) // With defaults this will remove population