Militaristic city states can give unique unit; refactor city state initialization (#5147)

* Pick unique unit for militaristic CS

* Provide unique unit when able

* refactor some code

* remove debug

* template.properties

* fix failing build

* Add variance to spawning rate, add ≈ to string, better formatting.
This commit is contained in:
SimonCeder
2021-09-09 20:30:12 +02:00
committed by GitHub
parent f1f4def7f0
commit 83050c2b11
7 changed files with 101 additions and 40 deletions

View File

@ -193,23 +193,17 @@ object GameStarter {
// and then all the other City-States in a random order! Because the sortedBy function is stable!
availableCityStatesNames.addAll(ruleset.nations.filter { it.value.isCityState() }.keys
.shuffled().sortedByDescending { it in civNamesWithStartingLocations })
val allMercantileResources = ruleset.tileResources.values.filter { it.unique == "Can only be created by Mercantile City-States" }.map { it.name }
val unusedMercantileResources = Stack<String>()
unusedMercantileResources.addAll(allMercantileResources.shuffled())
for (cityStateName in availableCityStatesNames.take(newGameParameters.numberOfCityStates)) {
var addedCityStates = 0
// Keep trying to add city states until we reach the target number.
while (addedCityStates < newGameParameters.numberOfCityStates) {
if (availableCityStatesNames.isEmpty()) // We ran out of city-states somehow
break
val cityStateName = availableCityStatesNames.pop()
val civ = CivilizationInfo(cityStateName)
civ.cityStatePersonality = CityStatePersonality.values().random()
civ.cityStateResource = when {
ruleset.nations[cityStateName]?.cityStateType != CityStateType.Mercantile -> null
allMercantileResources.isEmpty() -> null
unusedMercantileResources.empty() -> allMercantileResources.random() // When unused luxuries exhausted, random
else -> unusedMercantileResources.pop() // First pick an unused luxury if possible
if (civ.initCityState(ruleset, newGameParameters.startingEra, availableCivNames)) { // true if successful init
gameInfo.civilizations.add(civ)
addedCityStates++
}
gameInfo.civilizations.add(civ)
for (tech in startingTechs)
civ.tech.techsResearched.add(tech.name) // can't be .addTechnology because the civInfo isn't assigned yet
}
}

View File

@ -6,6 +6,7 @@ import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
import com.unciv.models.metadata.GameSpeed
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.stats.Stat
import com.unciv.models.translations.getPlaceholderParameters
import com.unciv.models.translations.getPlaceholderText
@ -18,6 +19,51 @@ import kotlin.math.pow
/** Class containing city-state-specific functions */
class CityStateFunctions(val civInfo: CivilizationInfo) {
/** Attempts to initialize the city state, returning true if successful. */
fun initCityState(ruleset: Ruleset, startingEra: String, unusedMajorCivs: Collection<String>): Boolean {
val cityStateType = ruleset.nations[civInfo.civName]?.cityStateType
if (cityStateType == null) return false
val startingTechs = ruleset.technologies.values.filter { it.uniques.contains("Starting tech") }
for (tech in startingTechs)
civInfo.tech.techsResearched.add(tech.name) // can't be .addTechnology because the civInfo isn't assigned yet
val allMercantileResources = ruleset.tileResources.values.filter { it.unique == "Can only be created by Mercantile City-States" }.map { it.name }
val allPossibleBonuses = HashSet<String>() // We look through these to determine what kind of city state we are
for (era in ruleset.eras.values) {
val allyBonuses = era.allyBonus[cityStateType.name]
val friendBonuses = era.friendBonus[cityStateType.name]
if (allyBonuses != null)
allPossibleBonuses.addAll(allyBonuses)
if (friendBonuses != null)
allPossibleBonuses.addAll(friendBonuses)
}
// CS Personality
civInfo.cityStatePersonality = CityStatePersonality.values().random()
// Mercantile bonus resources
if ("Provides a unique luxury" in allPossibleBonuses
|| (allPossibleBonuses.isEmpty() && cityStateType == CityStateType.Mercantile)) { // Fallback for badly defined Eras.json
civInfo.cityStateResource = allMercantileResources.random()
}
// Unique unit for militaristic city-states
if (allPossibleBonuses.any { it.getPlaceholderText() == "Provides military units every ≈[] turns" }
|| (allPossibleBonuses.isEmpty() && cityStateType == CityStateType.Militaristic)) { // Fallback for badly defined Eras.json
val possibleUnits = ruleset.units.values.filter { it.requiredTech != null
&& ruleset.eras[ruleset.technologies[it.requiredTech!!]!!.era()]!!.eraNumber > ruleset.eras[startingEra]!!.eraNumber // Not from the start era or before
&& it.uniqueTo != null && it.uniqueTo in unusedMajorCivs // Must be from a major civ not in the game
&& ruleset.unitTypes[it.unitType]!!.isLandUnit() && ( it.strength > 0 || it.rangedStrength > 0 ) } // Must be a land military unit
if (possibleUnits.isNotEmpty())
civInfo.cityStateUniqueUnit = possibleUnits.random().name
}
// TODO: Return false if attempting to put a religious city-state in a game without religion
return true
}
/** Gain a random great person from the city state */
fun giveGreatPersonToPatron(receivingCiv: CivilizationInfo) {
@ -37,7 +83,12 @@ class CityStateFunctions(val civInfo: CivilizationInfo) {
fun giveMilitaryUnitToPatron(receivingCiv: CivilizationInfo) {
val cities = NextTurnAutomation.getClosestCities(receivingCiv, civInfo)
val city = cities.city1
val militaryUnit = city.cityConstructions.getConstructableUnits()
val uniqueUnit = civInfo.gameInfo.ruleSet.units[civInfo.cityStateUniqueUnit]
// If the receiving civ has discovered the required tech and not the obsolete tech for our unique, always give them the unique
val militaryUnit = if (uniqueUnit != null && receivingCiv.tech.isResearched(uniqueUnit.requiredTech!!)
&& (uniqueUnit.obsoleteTech == null || !receivingCiv.tech.isResearched(uniqueUnit.obsoleteTech!!))) uniqueUnit
// Otherwise pick at random
else city.cityConstructions.getConstructableUnits()
.filter { !it.isCivilian() && it.isLandUnit() && it.uniqueTo==null }
.toList().random()
// placing the unit may fail - in that case stay quiet

View File

@ -174,6 +174,7 @@ class CivilizationInfo {
toReturn.naturalWonders.addAll(naturalWonders)
toReturn.cityStatePersonality = cityStatePersonality
toReturn.cityStateResource = cityStateResource
toReturn.cityStateUniqueUnit = cityStateUniqueUnit
toReturn.flagsCountdown.putAll(flagsCountdown)
toReturn.temporaryUniques.addAll(temporaryUniques)
toReturn.boughtConstructionsWithGloballyIncreasingPrice.putAll(boughtConstructionsWithGloballyIncreasingPrice)
@ -208,6 +209,7 @@ class CivilizationInfo {
val cityStateType: CityStateType get() = nation.cityStateType!!
var cityStatePersonality: CityStatePersonality = CityStatePersonality.Neutral
var cityStateResource: String? = null
var cityStateUniqueUnit: String? = null // Unique unit for militaristic city state. Might still be null if there are no appropriate units
fun isMajorCiv() = nation.isMajorCiv()
fun isAlive(): Boolean = !isDefeated()
fun hasEverBeenFriendWith(otherCiv: CivilizationInfo): Boolean = getDiplomacyManager(otherCiv).everBeenFriends()
@ -894,6 +896,9 @@ class CivilizationInfo {
}
//////////////////////// City State wrapper functions ////////////////////////
fun initCityState(ruleset: Ruleset, startingEra: String, unusedMajorCivs: Collection<String>)
= cityStateFunctions.initCityState(ruleset, startingEra, unusedMajorCivs)
/** Gain a random great person from the city state */
private fun giveGreatPersonToPatron(receivingCiv: CivilizationInfo) {
cityStateFunctions.giveGreatPersonToPatron(receivingCiv)

View File

@ -541,6 +541,8 @@ class DiplomacyManager() {
if (relationshipLevel() < RelationshipLevel.Friend) {
if (hasFlag(DiplomacyFlags.ProvideMilitaryUnit)) removeFlag(DiplomacyFlags.ProvideMilitaryUnit)
} else {
val variance = listOf(-1, 0, 1).random()
val relevantBonuses =
if (relationshipLevel() == RelationshipLevel.Friend)
eraInfo.friendBonus[otherCiv().cityStateType.name]
@ -549,19 +551,19 @@ class DiplomacyManager() {
if (relevantBonuses == null && otherCiv().cityStateType == CityStateType.Militaristic) {
// Deprecated, assume Civ V values for compatibility
if (!hasFlag(DiplomacyFlags.ProvideMilitaryUnit) && relationshipLevel() == RelationshipLevel.Friend)
setFlag(DiplomacyFlags.ProvideMilitaryUnit, 20)
setFlag(DiplomacyFlags.ProvideMilitaryUnit, 20 + variance)
if ((!hasFlag(DiplomacyFlags.ProvideMilitaryUnit) || getFlag(DiplomacyFlags.ProvideMilitaryUnit) > 17)
&& relationshipLevel() == RelationshipLevel.Ally)
setFlag(DiplomacyFlags.ProvideMilitaryUnit, 17)
setFlag(DiplomacyFlags.ProvideMilitaryUnit, 17 + variance)
}
if (relevantBonuses == null) return
for (bonus in relevantBonuses) {
// Reset the countdown if it has ended, or if we have longer to go than the current maximum (can happen when going from friend to ally)
if (bonus.getPlaceholderText() == "Provides military units every [] turns" &&
if (bonus.getPlaceholderText() == "Provides military units every [] turns" &&
(!hasFlag(DiplomacyFlags.ProvideMilitaryUnit) || getFlag(DiplomacyFlags.ProvideMilitaryUnit) > bonus.getPlaceholderParameters()[0].toInt()))
setFlag(DiplomacyFlags.ProvideMilitaryUnit, bonus.getPlaceholderParameters()[0].toInt())
setFlag(DiplomacyFlags.ProvideMilitaryUnit, bonus.getPlaceholderParameters()[0].toInt() + variance)
}
}
}

View File

@ -132,9 +132,9 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() {
val otherCivDiplomacyManager = otherCiv.getDiplomacyManager(viewingCiv)
val diplomacyTable = Table()
diplomacyTable.defaults().pad(10f)
diplomacyTable.defaults().pad(2.5f)
diplomacyTable.add(LeaderIntroTable(otherCiv)).row()
diplomacyTable.add(LeaderIntroTable(otherCiv)).padBottom(15f).row()
diplomacyTable.add("{Type}: {${otherCiv.cityStateType}}".toLabel()).row()
diplomacyTable.add("{Personality}: {${otherCiv.cityStatePersonality}}".toLabel()).row()
@ -162,6 +162,7 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() {
}
diplomacyTable.add(resourcesTable).row()
}
diplomacyTable.row().padTop(15f)
otherCiv.updateAllyCivForCityState()
val ally = otherCiv.getAllyCiv()
@ -186,6 +187,7 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() {
if (nextLevelString.isNotEmpty()) {
diplomacyTable.add(nextLevelString.toLabel()).row()
}
diplomacyTable.row().padTop(15f)
val eraInfo = viewingCiv.getEra()
@ -215,6 +217,12 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() {
.apply { setAlignment(Align.center) }
diplomacyTable.add(allyBonusLabel).row()
if (otherCiv.cityStateUniqueUnit != null) {
val unitName = otherCiv.cityStateUniqueUnit
val techName = viewingCiv.gameInfo.ruleSet.units[otherCiv.cityStateUniqueUnit]!!.requiredTech
diplomacyTable.add("[${otherCiv.civName}] is able to provide [${unitName}] once [${techName}] is researched.".toLabel(fontSize = 18)).row()
}
return diplomacyTable
}