Fix disbanding units a civ cannot afford (#9073)

* Fix disbanding units a civ cannot afford

* Avoid performance impact of CityState Afraid evaluation where possible
This commit is contained in:
SomeTroglodyte 2023-03-30 09:22:52 +02:00 committed by GitHub
parent c5d0de8144
commit 3d56c1ba5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 105 additions and 42 deletions

View File

@ -258,7 +258,7 @@ object NextTurnAutomation {
if (popupAlert.type == AlertType.DeclarationOfFriendship) {
val requestingCiv = civInfo.gameInfo.getCivilization(popupAlert.value)
val diploManager = civInfo.getDiplomacyManager(requestingCiv)
if (diploManager.relationshipLevel() > RelationshipLevel.Neutral
if (diploManager.isRelationshipLevelGT(RelationshipLevel.Neutral)
&& !diploManager.otherCivDiplomacy().hasFlag(DiplomacyFlags.Denunciation)) {
diploManager.signDeclarationOfFriendship()
requestingCiv.addNotification("We have signed a Declaration of Friendship with [${civInfo.civName}]!", NotificationCategory.Diplomacy, NotificationIcon.Diplomacy, civInfo.civName)
@ -377,23 +377,21 @@ object NextTurnAutomation {
}
private fun protectCityStates(civInfo: Civilization) {
for (state in civInfo.getKnownCivs().filter{!it.isDefeated() && it.isCityState()}) {
for (state in civInfo.getKnownCivs().filter { !it.isDefeated() && it.isCityState() }) {
val diplomacyManager = state.getDiplomacyManager(civInfo.civName)
if(diplomacyManager.relationshipLevel() >= RelationshipLevel.Friend
&& state.cityStateFunctions.otherCivCanPledgeProtection(civInfo))
{
val isAtLeastFriend = diplomacyManager.isRelationshipLevelGE(RelationshipLevel.Friend)
if (isAtLeastFriend && state.cityStateFunctions.otherCivCanPledgeProtection(civInfo)) {
state.cityStateFunctions.addProtectorCiv(civInfo)
} else if (diplomacyManager.relationshipLevel() < RelationshipLevel.Friend
&& state.cityStateFunctions.otherCivCanWithdrawProtection(civInfo)) {
} else if (!isAtLeastFriend && state.cityStateFunctions.otherCivCanWithdrawProtection(civInfo)) {
state.cityStateFunctions.removeProtectorCiv(civInfo)
}
}
}
private fun bullyCityStates(civInfo: Civilization) {
for (state in civInfo.getKnownCivs().filter{!it.isDefeated() && it.isCityState()}) {
for (state in civInfo.getKnownCivs().filter { !it.isDefeated() && it.isCityState() }) {
val diplomacyManager = state.getDiplomacyManager(civInfo.civName)
if(diplomacyManager.relationshipLevel() < RelationshipLevel.Friend
if (diplomacyManager.isRelationshipLevelLT(RelationshipLevel.Friend)
&& diplomacyManager.diplomaticStatus == DiplomaticStatus.Peace
&& valueCityStateAlliance(civInfo, state) <= 0
&& state.cityStateFunctions.getTributeWillingness(civInfo) >= 0) {
@ -652,8 +650,8 @@ object NextTurnAutomation {
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclinedLuxExchange)
}) {
val relationshipLevel = civInfo.getDiplomacyManager(otherCiv).relationshipLevel()
if (relationshipLevel <= RelationshipLevel.Enemy || otherCiv.tradeRequests.any { it.requestingCiv == civInfo.civName })
val isEnemy = civInfo.getDiplomacyManager(otherCiv).isRelationshipLevelLE(RelationshipLevel.Enemy)
if (isEnemy || otherCiv.tradeRequests.any { it.requestingCiv == civInfo.civName })
continue
val trades = potentialLuxuryTrades(civInfo, otherCiv)
@ -670,7 +668,7 @@ object NextTurnAutomation {
.asSequence()
.filter {
it.isMajorCiv() && !it.isAtWarWith(civInfo)
&& it.getDiplomacyManager(civInfo).relationshipLevel() > RelationshipLevel.Neutral
&& it.getDiplomacyManager(civInfo).isRelationshipLevelGT(RelationshipLevel.Neutral)
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.DeclarationOfFriendship)
&& !civInfo.getDiplomacyManager(it).hasFlag(DiplomacyFlags.Denunciation)
}
@ -804,7 +802,7 @@ object NextTurnAutomation {
if (diplomacyManager.hasFlag(DiplomacyFlags.DeclarationOfFriendship))
modifierMap["Declaration of Friendship"] = -10
val relationshipModifier = when (diplomacyManager.relationshipLevel()) {
val relationshipModifier = when (diplomacyManager.relationshipIgnoreAfraid()) {
RelationshipLevel.Unforgivable -> 10
RelationshipLevel.Enemy -> 5
RelationshipLevel.Ally -> -5 // this is so that ally + DoF is not too unbalanced -

View File

@ -298,7 +298,7 @@ class CityStateFunctions(val civInfo: Civilization) {
return (!civInfo.isDefeated()
&& civInfo.isCityState()
&& civInfo.cities.any()
&& civInfo.getDiplomacyManager(otherCiv).relationshipLevel() == RelationshipLevel.Ally
&& civInfo.getDiplomacyManager(otherCiv).isRelationshipLevelEQ(RelationshipLevel.Ally)
&& !otherCiv.getDiplomacyManager(civInfo).hasFlag(DiplomacyFlags.MarriageCooldown)
&& otherCiv.getMatchingUniques(UniqueType.CityStateCanBeBoughtForGold).any()
&& otherCiv.gold >= getDiplomaticMarriageCost())
@ -464,7 +464,7 @@ class CityStateFunctions(val civInfo: Civilization) {
if (diplomacy.hasFlag(DiplomacyFlags.AngerFreeIntrusion)) continue // They recently helped us
val unitsInBorder = otherCiv.units.getCivUnits().count { !it.isCivilian() && it.getTile().getOwner() == civInfo }
if (unitsInBorder > 0 && diplomacy.relationshipLevel() < RelationshipLevel.Friend) {
if (unitsInBorder > 0 && diplomacy.isRelationshipLevelLT(RelationshipLevel.Friend)) {
diplomacy.addInfluence(-10f)
if (!diplomacy.hasFlag(DiplomacyFlags.BorderConflict)) {
otherCiv.popupAlerts.add(PopupAlert(AlertType.BorderConflict, civInfo.civName))

View File

@ -22,13 +22,18 @@ import kotlin.math.min
enum class RelationshipLevel(val color: Color) {
// War is tested separately for the Diplomacy Screen. Colored RED.
Unforgivable(Color.FIREBRICK),
Afraid(Color(0x5300ffff)), // HSV(260,100,100)
Enemy(Color.YELLOW),
Afraid(Color(0x5300ffff)), // HSV(260,100,100)
Competitor(Color(0x1f998fff)), // HSV(175,80,60)
Neutral(Color(0x1bb371ff)), // HSV(154,85,70)
Favorable(Color(0x14cc3cff)), // HSV(133,90,80)
Friend(Color(0x2ce60bff)), // HSV(111,95,90)
Ally(Color.CHARTREUSE) // HSV(90,100,100)
;
operator fun plus(delta: Int): RelationshipLevel {
val newOrdinal = (ordinal + delta).coerceIn(0, values().size - 1)
return values()[newOrdinal]
}
}
enum class DiplomacyFlags {
@ -168,7 +173,69 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
return modifierSum
}
/** Related to [relationshipLevel], this compares with a specific outcome.
*
* It is cheap unless you ask such that Neutral / Afraid on a CityState need to be distinguished and influence is currently in 0 until 30.
* Thus it can be far cheaper than first retrieving [relationshipLevel] and then comparing.
*
* Readability shortcuts: [isRelationshipLevelEQ], [isRelationshipLevelGE], [isRelationshipLevelGT], [isRelationshipLevelLE], [isRelationshipLevelLT]
*
* @param comparesAs same as [RelationshipLevel.compareTo]
* @return `true` if [relationshipLevel] ().compareTo([level]) == [comparesAs] - or: when [comparesAs] > 0 only if [relationshipLevel] > [level] and so on.
*/
private fun compareRelationshipLevel(level: RelationshipLevel, comparesAs: Int): Boolean {
if (!civInfo.isCityState())
return relationshipLevel().compareTo(level) == comparesAs
return when(level) {
RelationshipLevel.Afraid -> when {
comparesAs < 0 -> getInfluence() < 0
comparesAs > 0 -> getInfluence() >= 30 || relationshipLevel() > level
else -> getInfluence().let { it >= 0 && it < 30 } && relationshipLevel() == level
}
RelationshipLevel.Neutral -> when {
comparesAs < 0 -> getInfluence() < 0 || relationshipLevel() < level
comparesAs > 0 -> getInfluence() >= 30
else -> getInfluence().let { it >= 0 && it < 30 } && relationshipLevel() == level
}
else ->
// Outside the potentially expensive questions, do it the easy way
relationshipLevel().compareTo(level) == comparesAs
}
}
/** @see compareRelationshipLevel */
fun isRelationshipLevelEQ(level: RelationshipLevel) =
compareRelationshipLevel(level, 0)
/** @see compareRelationshipLevel */
fun isRelationshipLevelLT(level: RelationshipLevel) =
compareRelationshipLevel(level, -1)
/** @see compareRelationshipLevel */
fun isRelationshipLevelGT(level: RelationshipLevel) =
compareRelationshipLevel(level, 1)
/** @see compareRelationshipLevel */
fun isRelationshipLevelLE(level: RelationshipLevel) =
if (level == RelationshipLevel.Ally) true
else compareRelationshipLevel(level + 1, -1)
/** @see compareRelationshipLevel */
fun isRelationshipLevelGE(level: RelationshipLevel) =
if (level == RelationshipLevel.Unforgivable) true
else compareRelationshipLevel(level + -1, 1)
/** Careful: Cheap unless this is a CityState and influence is in 0 until 30,
* where the distinction Neutral/Afraid gets expensive.
* @see compareRelationshipLevel
* @see relationshipIgnoreAfraid
*/
fun relationshipLevel(): RelationshipLevel {
val level = relationshipIgnoreAfraid()
return when {
level != RelationshipLevel.Neutral || !civInfo.isCityState() -> level
civInfo.cityStateFunctions.getTributeWillingness(otherCiv()) > 0 -> RelationshipLevel.Afraid
else -> RelationshipLevel.Neutral
}
}
/** Same as [relationshipLevel] but omits the distinction Neutral/Afraid, which can be _much_ cheaper */
fun relationshipIgnoreAfraid(): RelationshipLevel {
if (civInfo.isHuman() && otherCiv().isHuman())
return RelationshipLevel.Neutral // People make their own choices.
@ -176,9 +243,8 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
return otherCiv().getDiplomacyManager(civInfo).relationshipLevel()
if (civInfo.isCityState()) return when {
getInfluence() <= -30 || civInfo.isAtWarWith(otherCiv()) -> RelationshipLevel.Unforgivable
getInfluence() <= -30 -> RelationshipLevel.Unforgivable // getInfluence tests isAtWarWith
getInfluence() < 0 -> RelationshipLevel.Enemy
getInfluence() < 30 && civInfo.cityStateFunctions.getTributeWillingness(otherCiv()) > 0 -> RelationshipLevel.Afraid
getInfluence() >= 60 && civInfo.getAllyCiv() == otherCivName -> RelationshipLevel.Ally
getInfluence() >= 30 -> RelationshipLevel.Friend
else -> RelationshipLevel.Neutral
@ -209,8 +275,8 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
val dropPerTurn = getCityStateInfluenceDegrade()
return when {
dropPerTurn == 0f -> 0
relationshipLevel() >= RelationshipLevel.Ally -> ceil((getInfluence() - 60f) / dropPerTurn).toInt() + 1
relationshipLevel() >= RelationshipLevel.Friend -> ceil((getInfluence() - 30f) / dropPerTurn).toInt() + 1
isRelationshipLevelEQ(RelationshipLevel.Ally) -> ceil((getInfluence() - 60f) / dropPerTurn).toInt() + 1
isRelationshipLevelEQ(RelationshipLevel.Friend) -> ceil((getInfluence() - 30f) / dropPerTurn).toInt() + 1
else -> 0
}
}
@ -219,13 +285,13 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
@Suppress("unused") //todo Finish original intent (usage in uniques) or remove
fun matchesCityStateRelationshipFilter(filter: String): Boolean {
val relationshipLevel = relationshipLevel()
val relationshipLevel = relationshipIgnoreAfraid()
return when (filter) {
"Allied" -> relationshipLevel == RelationshipLevel.Ally
"Friendly" -> relationshipLevel == RelationshipLevel.Friend
"Enemy" -> relationshipLevel == RelationshipLevel.Enemy
"Unforgiving" -> relationshipLevel == RelationshipLevel.Unforgivable
"Neutral" -> relationshipLevel == RelationshipLevel.Neutral
"Neutral" -> isRelationshipLevelEQ(RelationshipLevel.Neutral)
else -> false
}
}
@ -357,7 +423,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
*/
fun isConsideredFriendlyTerritory(): Boolean {
if (civInfo.isCityState() &&
(relationshipLevel() >= RelationshipLevel.Friend || otherCiv().hasUnique(UniqueType.CityStateTerritoryAlwaysFriendly)))
(isRelationshipLevelGE(RelationshipLevel.Friend) || otherCiv().hasUnique(UniqueType.CityStateTerritoryAlwaysFriendly)))
return true
return otherCivDiplomacy().hasOpenBorders // if THEY can enter US then WE are considered friendly territory for THEM
@ -435,7 +501,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
}
private fun nextTurnCityStateInfluence() {
val initialRelationshipLevel = relationshipLevel()
val initialRelationshipLevel = relationshipIgnoreAfraid() // Enough since only >= Friend is notified
val restingPoint = getCityStateInfluenceRestingPoint()
// We don't use `getInfluence()` here, as then during war with the ally of this CS,
@ -458,7 +524,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
else otherCiv().addNotification(text, NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy)
}
if (initialRelationshipLevel >= RelationshipLevel.Friend && initialRelationshipLevel != relationshipLevel()) {
if (initialRelationshipLevel >= RelationshipLevel.Friend && initialRelationshipLevel != relationshipIgnoreAfraid()) {
val text = "Your relationship with [${civInfo.civName}] degraded"
if (civCapitalLocation != null) otherCiv().addNotification(text, civCapitalLocation, NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy)
else otherCiv().addNotification(text, NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy)
@ -622,7 +688,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
if (!otherCiv().isCityState()) return
if (relationshipLevel() < RelationshipLevel.Friend) {
if (isRelationshipLevelLT(RelationshipLevel.Friend)) {
if (hasFlag(DiplomacyFlags.ProvideMilitaryUnit))
removeFlag(DiplomacyFlags.ProvideMilitaryUnit)
return
@ -630,7 +696,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
val variance = listOf(-1, 0, 1).random()
val provideMilitaryUnitUniques = civInfo.cityStateFunctions.getCityStateBonuses(otherCiv().cityStateType, relationshipLevel(), UniqueType.CityStateMilitaryUnits)
val provideMilitaryUnitUniques = civInfo.cityStateFunctions.getCityStateBonuses(otherCiv().cityStateType, relationshipIgnoreAfraid(), UniqueType.CityStateMilitaryUnits)
.filter { it.conditionalsApply(civInfo) }.toList()
if (provideMilitaryUnitUniques.isEmpty()) removeFlag(DiplomacyFlags.ProvideMilitaryUnit)
@ -846,7 +912,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
removeModifier(DiplomaticModifiers.DeclaredFriendshipWithOurEnemies)
for (thirdCiv in getCommonKnownCivs()
.filter { it.getDiplomacyManager(civInfo).hasFlag(DiplomacyFlags.DeclarationOfFriendship) }) {
when (otherCiv().getDiplomacyManager(thirdCiv).relationshipLevel()) {
when (otherCiv().getDiplomacyManager(thirdCiv).relationshipIgnoreAfraid()) {
RelationshipLevel.Unforgivable -> addModifier(DiplomaticModifiers.DeclaredFriendshipWithOurEnemies, -15f)
RelationshipLevel.Enemy -> addModifier(DiplomaticModifiers.DeclaredFriendshipWithOurEnemies, -5f)
RelationshipLevel.Friend -> addModifier(DiplomaticModifiers.DeclaredFriendshipWithOurAllies, 5f)
@ -869,7 +935,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
getCommonKnownCivs().filter { it.isMajorCiv() }.forEach { thirdCiv ->
thirdCiv.addNotification("[${civInfo.civName}] has denounced [$otherCivName]!",
NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy, otherCivName)
val thirdCivRelationshipWithOtherCiv = thirdCiv.getDiplomacyManager(otherCiv()).relationshipLevel()
val thirdCivRelationshipWithOtherCiv = thirdCiv.getDiplomacyManager(otherCiv()).relationshipIgnoreAfraid()
val thirdCivDiplomacyManager = thirdCiv.getDiplomacyManager(civInfo)
when (thirdCivRelationshipWithOtherCiv) {
RelationshipLevel.Unforgivable -> thirdCivDiplomacyManager.addModifier(DiplomaticModifiers.DenouncedOurEnemies, 15f)

View File

@ -245,15 +245,15 @@ class TurnManager(val civInfo: Civilization) {
// disband units until there are none left OR the gold values are normal
if (!civInfo.isBarbarian() && civInfo.gold <= -200 && nextTurnStats.gold.toInt() < 0) {
val militaryUnits = civInfo.units.getCivUnits().filter { it.isMilitary() }
do {
val militaryUnits = civInfo.units.getCivUnits().filter { it.isMilitary() } // New sequence as disband replaces unitList
val unitToDisband = militaryUnits.minByOrNull { it.baseUnit.cost }
// or .firstOrNull()?
?: break
unitToDisband.disband()
val unitName = unitToDisband.shortDisplayName()
civInfo.addNotification("Cannot provide unit upkeep for $unitName - unit has been disbanded!", NotificationCategory.Units, unitName, NotificationIcon.Death)
civInfo.updateStatsForNextTurn() // recalculate unit upkeep
// No need to recalculate unit upkeep, disband did that in UnitManager.removeUnit
nextTurnStats = civInfo.stats.statsForNextTurn
} while (civInfo.gold <= -200 && nextTurnStats.gold.toInt() < 0)
}

View File

@ -171,9 +171,8 @@ class CivInfoStatsForNextTurn(val civInfo: Civilization) {
//City-States bonuses
for (otherCiv in civInfo.getKnownCivs()) {
if (!otherCiv.isCityState()) continue
if (otherCiv.getDiplomacyManager(civInfo.civName)
.relationshipLevel() != RelationshipLevel.Ally
) continue
if (!otherCiv.getDiplomacyManager(civInfo.civName).isRelationshipLevelEQ(RelationshipLevel.Ally))
continue
for (unique in civInfo.getMatchingUniques(UniqueType.CityStateStatPercent)) {
statMap.add(
Constants.cityStates,

View File

@ -70,7 +70,7 @@ class TradeEvaluation {
// If we're making a peace treaty, don't try to up the bargain for people you don't like.
// Leads to spartan behaviour where you demand more, the more you hate the enemy...unhelpful
if (trade.ourOffers.none { it.name == Constants.peaceTreaty || it.name == Constants.researchAgreement }) {
val relationshipLevel = evaluator.getDiplomacyManager(tradePartner).relationshipLevel()
val relationshipLevel = evaluator.getDiplomacyManager(tradePartner).relationshipIgnoreAfraid()
if (relationshipLevel == RelationshipLevel.Enemy) sumOfOurOffers = (sumOfOurOffers * 1.5).toInt()
else if (relationshipLevel == RelationshipLevel.Unforgivable) sumOfOurOffers *= 2
}
@ -259,7 +259,7 @@ class TradeEvaluation {
}
TradeType.Agreement -> {
if (offer.name == Constants.openBorders) {
return when (civInfo.getDiplomacyManager(tradePartner).relationshipLevel()) {
return when (civInfo.getDiplomacyManager(tradePartner).relationshipIgnoreAfraid()) {
RelationshipLevel.Unforgivable -> 10000
RelationshipLevel.Enemy -> 2000
RelationshipLevel.Competitor -> 500

View File

@ -256,7 +256,7 @@ class DiplomacyScreen(
val allyBonusObjects = viewingCiv.cityStateFunctions.getCityStateBonuses(otherCiv.cityStateType, RelationshipLevel.Ally)
allyBonusText += allyBonusObjects.joinToString(separator = "\n") { it.text.tr() }
val relationLevel = otherCivDiplomacyManager.relationshipLevel()
val relationLevel = otherCivDiplomacyManager.relationshipIgnoreAfraid()
if (relationLevel >= RelationshipLevel.Friend) {
// RelationshipChange = Ally -> Friend or Friend -> Favorable
val turnsToRelationshipChange = otherCivDiplomacyManager.getTurnsToRelationshipChange()
@ -637,7 +637,7 @@ class DiplomacyScreen(
val diplomacyTable = Table()
diplomacyTable.defaults().pad(10f)
val helloText = if (otherCivDiplomacyManager.relationshipLevel() <= RelationshipLevel.Enemy)
val helloText = if (otherCivDiplomacyManager.isRelationshipLevelLE(RelationshipLevel.Enemy))
otherCiv.nation.hateHello
else otherCiv.nation.neutralHello
val leaderIntroTable = LeaderIntroTable(otherCiv, helloText)

View File

@ -217,7 +217,7 @@ class GlobalPoliticsOverviewTable (
//allied CS
for (cityState in gameInfo.getAliveCityStates()) {
if (cityState.diplomacy[civ.civName]?.relationshipLevel() == RelationshipLevel.Ally) {
if (cityState.diplomacy[civ.civName]?.isRelationshipLevelEQ(RelationshipLevel.Ally) == true) {
val alliedText = "Allied with [${getCivName(cityState)}]".toLabel()
alliedText.color = Color.GREEN
politicsTable.add(alliedText).row()

View File

@ -293,13 +293,13 @@ class AlertPopup(val worldScreen: WorldScreen, val popupAlert: PopupAlert): Popu
val player = worldScreen.viewingCiv
addLeaderName(bullyOrAttacker)
val relation = bullyOrAttacker.getDiplomacyManager(player).relationshipLevel()
val isAtLeastNeutral = bullyOrAttacker.getDiplomacyManager(player).isRelationshipLevelGE(RelationshipLevel.Neutral)
val text = when {
popupAlert.type == AlertType.BulliedProtectedMinor && relation >= RelationshipLevel.Neutral -> // Nice message
popupAlert.type == AlertType.BulliedProtectedMinor && isAtLeastNeutral -> // Nice message
"I've been informed that my armies have taken tribute from [${cityState.civName}], a city-state under your protection.\nI assure you, this was quite unintentional, and I hope that this does not serve to drive us apart."
popupAlert.type == AlertType.BulliedProtectedMinor -> // Nasty message
"We asked [${cityState.civName}] for a tribute recently and they gave in.\nYou promised to protect them from such things, but we both know you cannot back that up."
relation >= RelationshipLevel.Neutral -> // Nice message
isAtLeastNeutral -> // Nice message
"It's come to my attention that I may have attacked [${cityState.civName}], a city-state under your protection.\nWhile it was not my goal to be at odds with your empire, this was deemed a necessary course of action."
else -> // Nasty message
"I thought you might like to know that I've launched an invasion of one of your little pet states.\nThe lands of [${cityState.civName}] will make a fine addition to my own."