From f65709ab09502ce8d0a993b5e3debd93a528a99b Mon Sep 17 00:00:00 2001 From: Oskar Niesen Date: Sat, 22 Jun 2024 14:23:38 -0500 Subject: [PATCH] Add personality uses (#11807) * Reduced max motivationToAttack from relative combat strength * Refactored MotivationToAttackAutomation to use a float instead of an int * Added personality values to MotivationToAttackAutomation * Fixed minor build errors * Added DiplomacyAutomation personality modifiers * Improved ConstructionAutomation personality values * Added some more personality implementations * Tweaked trainSettler * Fixed civilizations asking for help against city-states * Adjusted DiplomacyAutomation to account for war mongering * Fixed typo --- .../com/unciv/logic/automation/Automation.kt | 4 +- .../automation/city/ConstructionAutomation.kt | 14 +- .../civilization/DeclareWarPlanEvaluator.kt | 62 ++--- .../DeclareWarTargetAutomation.kt | 12 +- .../civilization/DiplomacyAutomation.kt | 60 +++-- .../MotivationToAttackAutomation.kt | 223 +++++++++--------- .../civilization/NextTurnAutomation.kt | 13 +- .../civilization/UseGoldAutomation.kt | 2 +- .../civilization/diplomacy/DeclareWar.kt | 9 +- core/src/com/unciv/logic/trade/Trade.kt | 5 +- .../com/unciv/logic/trade/TradeEvaluation.kt | 10 +- .../models/ruleset/nation/Personality.kt | 18 +- 12 files changed, 238 insertions(+), 194 deletions(-) diff --git a/core/src/com/unciv/logic/automation/Automation.kt b/core/src/com/unciv/logic/automation/Automation.kt index fd7cdfe795..042f200ccf 100644 --- a/core/src/com/unciv/logic/automation/Automation.kt +++ b/core/src/com/unciv/logic/automation/Automation.kt @@ -233,8 +233,8 @@ object Automation { if (civInfo.gameInfo.turns > 120 * speed.barbarianModifier * multiplier) multiplier /= 2 - // If we have a lot of, or no cities we are not afraid - if (civInfo.cities.isEmpty() || civInfo.cities.size >= 4 * multiplier) + // If we have no cities or a lot of units we are not afraid + if (civInfo.cities.isEmpty() || civInfo.units.getCivUnits().count() >= 4 * multiplier) return false // If we have vision of our entire starting continent (ish) we are not afraid diff --git a/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt b/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt index 04607b77bb..dbfb2602fd 100644 --- a/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt +++ b/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt @@ -183,7 +183,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) { 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 - modifier *= personality.scaledFocus(PersonalityValue.Aggressive) + modifier *= personality.modifierFocus(PersonalityValue.Military, .3f) addChoice(relativeCostEffectiveness, militaryUnit, modifier) } @@ -256,7 +256,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) { if (!civInfo.hasUnique(UniqueType.EnablesConstructionOfSpaceshipParts)) return val spaceshipPart = (nonWonders + units).filter { it.name in spaceshipParts }.filterBuildable().firstOrNull() ?: return - val modifier = 2f + val modifier = 3f * personality.modifierFocus(PersonalityValue.Science, .4f) addChoice(relativeCostEffectiveness, spaceshipPart.name, modifier) } @@ -287,8 +287,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) { var value = 0f if (!cityIsOverAverageProduction) return value if (building.isWonder) value += 2f - if (building.hasUnique(UniqueType.TriggersCulturalVictory)) value += 10f - if (building.hasUnique(UniqueType.EnablesConstructionOfSpaceshipParts)) value += 10f + if (building.hasUnique(UniqueType.TriggersCulturalVictory)) value += 10f * personality.modifierFocus(PersonalityValue.Culture, .3f) + if (building.hasUnique(UniqueType.EnablesConstructionOfSpaceshipParts)) value += 10f * personality.modifierFocus(PersonalityValue.Science, .3f) return value } @@ -300,8 +300,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) { .mapNotNull { NextTurnAutomation.getClosestCities(civInfo, it) } .any { it.city1 == city }) warModifier *= 2f - value += warModifier * building.cityHealth.toFloat() / city.getMaxHealth() - value += warModifier * building.cityStrength.toFloat() / (city.getStrength() + 3) // The + 3 here is to reduce the priority of building walls immedietly + value += warModifier * building.cityHealth.toFloat() / city.getMaxHealth() * personality.inverseModifierFocus(PersonalityValue.Aggressive, .3f) + value += warModifier * building.cityStrength.toFloat() / (city.getStrength() + 3) * personality.inverseModifierFocus(PersonalityValue.Aggressive, .3f) // The + 3 here is to reduce the priority of building walls immedietly for (experienceUnique in building.getMatchingUniques(UniqueType.UnitStartingExperience, cityState)) { var modifier = experienceUnique.params[1].toFloat() / 5 @@ -346,7 +346,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) { if (civInfo.wantsToFocusOn(stat)) buildingStats[stat] *= 2f - buildingStats[stat] *= personality.scaledFocus(PersonalityValue[stat]) + buildingStats[stat] *= personality.modifierFocus(PersonalityValue[stat], .5f) } return Automation.rankStatsValue(civInfo.getPersonality().scaleStats(buildingStats.clone(), .3f), civInfo) diff --git a/core/src/com/unciv/logic/automation/civilization/DeclareWarPlanEvaluator.kt b/core/src/com/unciv/logic/automation/civilization/DeclareWarPlanEvaluator.kt index 121097a3f3..5e4c4b8245 100644 --- a/core/src/com/unciv/logic/automation/civilization/DeclareWarPlanEvaluator.kt +++ b/core/src/com/unciv/logic/automation/civilization/DeclareWarPlanEvaluator.kt @@ -18,18 +18,18 @@ object DeclareWarPlanEvaluator { * This style of declaring war favors fighting stronger civilizations. * @return The movtivation of the plan. If it is > 0 then we can declare the war. */ - fun evaluateTeamWarPlan(civInfo: Civilization, target: Civilization, teamCiv: Civilization, givenMotivation: Int?): Int { + fun evaluateTeamWarPlan(civInfo: Civilization, target: Civilization, teamCiv: Civilization, givenMotivation: Float?): Float { val teamCivDiplo = civInfo.getDiplomacyManager(teamCiv)!! - if (civInfo.getPersonality()[PersonalityValue.DeclareWar] == 0f) return -1000 - if (teamCivDiplo.isRelationshipLevelLT(RelationshipLevel.Neutral)) return -1000 + if (civInfo.getPersonality()[PersonalityValue.DeclareWar] == 0f) return -1000f + if (teamCivDiplo.isRelationshipLevelLT(RelationshipLevel.Neutral)) return -1000f var motivation = givenMotivation - ?: MotivationToAttackAutomation.hasAtLeastMotivationToAttack(civInfo, target, 0) + ?: MotivationToAttackAutomation.hasAtLeastMotivationToAttack(civInfo, target, 0f) - if (teamCivDiplo.isRelationshipLevelEQ(RelationshipLevel.Neutral)) motivation -= 5 + if (teamCivDiplo.isRelationshipLevelEQ(RelationshipLevel.Neutral)) motivation -= 5f // Make sure that they can actually help us with the target if (!teamCiv.threatManager.getNeighboringCivilizations().contains(target)) { - motivation -= 40 + motivation -= 40f } val civForce = civInfo.getStatForRanking(RankingType.Force) @@ -46,11 +46,11 @@ object DeclareWarPlanEvaluator { if (civForce + teamCivForce < targetForce * multiplier) { // We are weaker then them even with our combined forces // If they have twice our combined force we will have -30 motivation - motivation -= (30 * ((targetForce * multiplier) / (teamCivForce + civForce) - 1)).toInt() + motivation -= 30 * ((targetForce * multiplier) / (teamCivForce + civForce) - 1) } else if (civForce + teamCivForce > targetForce * 2) { // Why gang up on such a weaker enemy when we can declare war ourselves? // If our combined force is twice their force we will have -20 motivation - motivation -= (20 * ((civForce + teamCivForce) / targetForce * 2) - 1).toInt() + motivation -= 20 * ((civForce + teamCivForce) / targetForce * 2) - 1 } val civScore = civInfo.getStatForRanking(RankingType.Score) @@ -59,9 +59,9 @@ object DeclareWarPlanEvaluator { if (teamCivScore > civScore * 1.4f && teamCivScore >= targetCivScore) { // If teamCiv has more score than us and the target they are likely in a good position already - motivation -= (20 * ((teamCivScore / (civScore * 1.4f)) - 1)).toInt() + motivation -= 20 * ((teamCivScore / (civScore * 1.4f)) - 1) } - return motivation - 20 + return motivation - 20f } /** @@ -70,20 +70,20 @@ object DeclareWarPlanEvaluator { * Favors protecting allies. * @return The movtivation of the plan. If it is > 0 then we can declare the war. */ - fun evaluateJoinWarPlan(civInfo: Civilization, target: Civilization, civToJoin: Civilization, givenMotivation: Int?): Int { + fun evaluateJoinWarPlan(civInfo: Civilization, target: Civilization, civToJoin: Civilization, givenMotivation: Float?): Float { val thirdCivDiplo = civInfo.getDiplomacyManager(civToJoin)!! - if (civInfo.getPersonality()[PersonalityValue.DeclareWar] == 0f) return -1000 - if (thirdCivDiplo.isRelationshipLevelLE(RelationshipLevel.Favorable)) return -1000 + if (civInfo.getPersonality()[PersonalityValue.DeclareWar] == 0f) return -1000f + if (thirdCivDiplo.isRelationshipLevelLE(RelationshipLevel.Favorable)) return -1000f var motivation = givenMotivation - ?: MotivationToAttackAutomation.hasAtLeastMotivationToAttack(civInfo, target, 0) + ?: MotivationToAttackAutomation.hasAtLeastMotivationToAttack(civInfo, target, 0f) // We need to be able to trust the thirdCiv at least somewhat if (thirdCivDiplo.diplomaticStatus != DiplomaticStatus.DefensivePact && thirdCivDiplo.opinionOfOtherCiv() + motivation * 2 < 80) { - motivation -= 80 - (thirdCivDiplo.opinionOfOtherCiv() + motivation * 2).toInt() + motivation -= 80f - thirdCivDiplo.opinionOfOtherCiv() + motivation * 2 } if (!civToJoin.threatManager.getNeighboringCivilizations().contains(target)) { - motivation -= 20 + motivation -= 20f } val targetForce = target.getStatForRanking(RankingType.Force) - 0.8f * target.getCivsAtWarWith().sumOf { it.getStatForRanking(RankingType.Force) }.coerceAtLeast(100) @@ -93,7 +93,7 @@ object DeclareWarPlanEvaluator { val civToJoinForce = (civToJoin.getStatForRanking(RankingType.Force) - 0.8f * civToJoin.getCivsAtWarWith().sumOf { it.getStatForRanking(RankingType.Force) }).coerceAtLeast(100f) if (civToJoinForce < targetForce / 2) { // Make sure that there is no wrap around - motivation -= (10 * (targetForce / civToJoinForce)).toInt().coerceIn(-1000, 1000) + motivation -= 10 * (targetForce / civToJoinForce).coerceIn(-1000f, 1000f) } // A higher motivation means that we can be riskier @@ -105,7 +105,7 @@ object DeclareWarPlanEvaluator { else -> 0.8f } if (civToJoinForce + civForce < targetForce * multiplier) { - motivation -= (20 * (targetForce * multiplier) / (civToJoinForce + civForce)).toInt().coerceIn(-1000, 1000) + motivation -= 20 * (targetForce * multiplier) / (civToJoinForce + civForce).coerceIn(-1000f, 1000f) } return motivation - 15 @@ -116,11 +116,11 @@ object DeclareWarPlanEvaluator { * * @return The movtivation of the plan. If it is > 0 then we can declare the war. */ - fun evaluateJoinOurWarPlan(civInfo: Civilization, target: Civilization, civToJoin: Civilization, givenMotivation: Int?): Int { - if (civInfo.getDiplomacyManager(civToJoin)!!.isRelationshipLevelLT(RelationshipLevel.Favorable)) return -1000 - var motivation = givenMotivation ?: 0 + fun evaluateJoinOurWarPlan(civInfo: Civilization, target: Civilization, civToJoin: Civilization, givenMotivation: Float?): Float { + if (civInfo.getDiplomacyManager(civToJoin)!!.isRelationshipLevelLT(RelationshipLevel.Favorable)) return -1000f + var motivation = givenMotivation ?: 0f if (!civToJoin.threatManager.getNeighboringCivilizations().contains(target)) { - motivation -= 50 + motivation -= 50f } val targetForce = target.getStatForRanking(RankingType.Force) @@ -128,10 +128,10 @@ object DeclareWarPlanEvaluator { // They need to be at least half the targets size val thirdCivForce = (civToJoin.getStatForRanking(RankingType.Force) - 0.8f * civToJoin.getCivsAtWarWith().sumOf { it.getStatForRanking(RankingType.Force) }).coerceAtLeast(100f) - motivation += (20 * thirdCivForce / targetForce.toFloat()).toInt().coerceAtMost(40) + motivation += 20 * thirdCivForce / targetForce.toFloat().coerceAtMost(40f) // If we have less relative force then the target then we have more motivation to accept - motivation += (30 * (1 - (civForce / targetForce.toFloat()))).toInt().coerceIn(-30, 30) + motivation += 30 * (1 - (civForce / targetForce.toFloat())).coerceIn(-30f, 30f) return motivation - 20 } @@ -142,15 +142,15 @@ object DeclareWarPlanEvaluator { * * @return The movtivation of the plan. If it is > 0 then we can declare the war. */ - fun evaluateDeclareWarPlan(civInfo: Civilization, target: Civilization, givenMotivation: Int?): Int { - if (civInfo.getPersonality()[PersonalityValue.DeclareWar] == 0f) return -1000 + fun evaluateDeclareWarPlan(civInfo: Civilization, target: Civilization, givenMotivation: Float?): Float { + if (civInfo.getPersonality()[PersonalityValue.DeclareWar] == 0f) return -1000f val motivation = givenMotivation - ?: MotivationToAttackAutomation.hasAtLeastMotivationToAttack(civInfo, target, 0) + ?: MotivationToAttackAutomation.hasAtLeastMotivationToAttack(civInfo, target, 0f) val diploManager = civInfo.getDiplomacyManager(target)!! if (diploManager.hasFlag(DiplomacyFlags.WaryOf) && diploManager.getFlag(DiplomacyFlags.WaryOf) < 0) { - val turnsToPlan = (10 - (motivation / 10)).coerceAtLeast(3) + val turnsToPlan = (10 - (motivation / 10)).coerceAtLeast(3f) val turnsToWait = turnsToPlan + diploManager.getFlag(DiplomacyFlags.WaryOf) return motivation - turnsToWait * 3 } @@ -163,14 +163,14 @@ object DeclareWarPlanEvaluator { * * @return The motivation of the plan. If it is > 0 then we can start planning the war. */ - fun evaluateStartPreparingWarPlan(civInfo: Civilization, target: Civilization, givenMotivation: Int?): Int { + fun evaluateStartPreparingWarPlan(civInfo: Civilization, target: Civilization, givenMotivation: Float?): Float { val motivation = givenMotivation - ?: MotivationToAttackAutomation.hasAtLeastMotivationToAttack(civInfo, target, 0) + ?: MotivationToAttackAutomation.hasAtLeastMotivationToAttack(civInfo, target, 0f) // TODO: We use negative values in WaryOf for now so that we aren't adding any extra fields to the save file // This will very likely change in the future and we will want to build upon it val diploManager = civInfo.getDiplomacyManager(target)!! - if (diploManager.hasFlag(DiplomacyFlags.WaryOf)) return 0 + if (diploManager.hasFlag(DiplomacyFlags.WaryOf)) return 0f return motivation - 15 } diff --git a/core/src/com/unciv/logic/automation/civilization/DeclareWarTargetAutomation.kt b/core/src/com/unciv/logic/automation/civilization/DeclareWarTargetAutomation.kt index 7a479cc5ba..45e2157289 100644 --- a/core/src/com/unciv/logic/automation/civilization/DeclareWarTargetAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/DeclareWarTargetAutomation.kt @@ -15,7 +15,7 @@ object DeclareWarTargetAutomation { * Chooses a target civilization along with a plan of attack. * Note that this doesn't guarantee that we will declare war on them immediatly, or that we will end up declaring war at all. */ - fun chooseDeclareWarTarget(civInfo: Civilization, civAttackMotivations: List>) { + fun chooseDeclareWarTarget(civInfo: Civilization, civAttackMotivations: List>) { val highestValueTargets = civAttackMotivations.sortedByDescending { it.first.getStatForRanking(RankingType.Score) } for (target in highestValueTargets) { @@ -27,7 +27,7 @@ object DeclareWarTargetAutomation { /** * Determines a war plan against this [target] and executes it if able. */ - private fun tryDeclareWarWithPlan(civInfo: Civilization, target: Civilization, motivation: Int): Boolean { + private fun tryDeclareWarWithPlan(civInfo: Civilization, target: Civilization, motivation: Float): Boolean { if (!target.isCityState()) { if (motivation > 5 && tryTeamWar(civInfo, target, motivation)) return true @@ -46,7 +46,7 @@ object DeclareWarTargetAutomation { * The safest option for war is to invite a new ally to join the war with us. * Together we are stronger and are more likely to take down bigger threats. */ - private fun tryTeamWar(civInfo: Civilization, target: Civilization, motivation: Int): Boolean { + private fun tryTeamWar(civInfo: Civilization, target: Civilization, motivation: Float): Boolean { val potentialAllies = civInfo.getDiplomacyManager(target)!!.getCommonKnownCivs() .filter { it.isMajorCiv() @@ -74,7 +74,7 @@ object DeclareWarTargetAutomation { /** * The next safest aproach is to join an existing war on the side of an ally that is already at war with [target]. */ - private fun tryJoinWar(civInfo: Civilization, target: Civilization, motivation: Int): Boolean { + private fun tryJoinWar(civInfo: Civilization, target: Civilization, motivation: Float): Boolean { val potentialAllies = civInfo.getDiplomacyManager(target)!!.getCommonKnownCivs() .filter { it.isMajorCiv() @@ -102,7 +102,7 @@ object DeclareWarTargetAutomation { /** * Lastly, if our motivation is high enough and we don't have any better plans then lets just declare war. */ - private fun declareWar(civInfo: Civilization, target: Civilization, motivation: Int): Boolean { + private fun declareWar(civInfo: Civilization, target: Civilization, motivation: Float): Boolean { if (DeclareWarPlanEvaluator.evaluateDeclareWarPlan(civInfo, target, motivation) > 0) { civInfo.getDiplomacyManager(target)!!.declareWar() return true @@ -113,7 +113,7 @@ object DeclareWarTargetAutomation { /** * Slightly safter is to silently plan an invasion and declare war later. */ - private fun prepareWar(civInfo: Civilization, target: Civilization, motivation: Int): Boolean { + private fun prepareWar(civInfo: Civilization, target: Civilization, motivation: Float): Boolean { // TODO: We use negative values in WaryOf for now so that we aren't adding any extra fields to the save file // This will very likely change in the future and we will want to build upon it val diploManager = civInfo.getDiplomacyManager(target)!! diff --git a/core/src/com/unciv/logic/automation/civilization/DiplomacyAutomation.kt b/core/src/com/unciv/logic/automation/civilization/DiplomacyAutomation.kt index 3cf674b6b8..2b1049bc06 100644 --- a/core/src/com/unciv/logic/automation/civilization/DiplomacyAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/DiplomacyAutomation.kt @@ -8,6 +8,7 @@ import com.unciv.logic.civilization.AlertType import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.PopupAlert import com.unciv.logic.civilization.diplomacy.DiplomacyFlags +import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers import com.unciv.logic.civilization.diplomacy.DiplomaticStatus import com.unciv.logic.civilization.diplomacy.RelationshipLevel import com.unciv.logic.trade.TradeEvaluation @@ -33,7 +34,8 @@ object DiplomacyAutomation { .sortedByDescending { it.getDiplomacyManager(civInfo)!!.relationshipLevel() }.toList() for (otherCiv in civsThatWeCanDeclareFriendshipWith) { // Default setting is 2, this will be changed according to different civ. - if ((1..10).random() <= 2 && wantsToSignDeclarationOfFrienship(civInfo, otherCiv)) { + if ((1..10).random() <= 2 * civInfo.getPersonality().modifierFocus(PersonalityValue.Diplomacy, .5f) + && wantsToSignDeclarationOfFrienship(civInfo, otherCiv)) { otherCiv.popupAlerts.add(PopupAlert(AlertType.DeclarationOfFriendship, civInfo.civName)) } } @@ -53,7 +55,12 @@ object DiplomacyAutomation { val allAliveCivs = allCivs - deadCivs // Motivation should be constant as the number of civs changes - var motivation = diploManager.opinionOfOtherCiv().toInt() - 40 + var motivation = diploManager.opinionOfOtherCiv() - 40f + + // Warmongerers don't make good allies + if (diploManager.hasModifier(DiplomaticModifiers.WarMongerer)) { + motivation -= diploManager.getModifier(DiplomaticModifiers.WarMongerer) * civInfo.getPersonality().modifierFocus(PersonalityValue.Diplomacy, .5f) + } // If the other civ is stronger than we are compelled to be nice to them // If they are too weak, then thier friendship doesn't mean much to us @@ -65,13 +72,13 @@ object DiplomacyAutomation { } // Try to ally with a fourth of the civs in play - val civsToAllyWith = 0.25f * allAliveCivs + val civsToAllyWith = 0.25f * allAliveCivs * civInfo.getPersonality().modifierFocus(PersonalityValue.Diplomacy, .3f) if (numOfFriends < civsToAllyWith) { // Goes from 10 to 0 once the civ gets 1/4 of all alive civs as friends - motivation += (10 - 10 * (numOfFriends / civsToAllyWith)).toInt() + motivation += (10 - 10 * numOfFriends / civsToAllyWith) } else { // Goes from 0 to -120 as the civ gets more friends, offset by civsToAllyWith - motivation -= (120f * (numOfFriends - civsToAllyWith) / (knownCivs - civsToAllyWith)).toInt() + motivation -= (120f * (numOfFriends - civsToAllyWith) / (knownCivs - civsToAllyWith)) } // The more friends they have the less we should want to sign friendship (To promote teams) @@ -88,7 +95,7 @@ object DiplomacyAutomation { // Wait to declare frienships until more civs // Goes from -30 to 0 when we know 75% of allCivs val civsToKnow = 0.75f * allAliveCivs - motivation -= ((civsToKnow - knownCivs) / civsToKnow * 30f).toInt().coerceAtLeast(0) + motivation -= ((civsToKnow - knownCivs) / civsToKnow * 30f).coerceAtLeast(0f) // If they are the only non-friendly civ near us then they are the only civ to attack and expand into if (civInfo.threatManager.getNeighboringCivilizations().none { @@ -97,7 +104,7 @@ object DiplomacyAutomation { }) motivation -= 20 - motivation -= hasAtLeastMotivationToAttack(civInfo, otherCiv, motivation / 2) * 2 + motivation -= hasAtLeastMotivationToAttack(civInfo, otherCiv, motivation / 2f) * 2 return motivation > 0 } @@ -139,7 +146,8 @@ object DiplomacyAutomation { // Being able to see their cities can give us an advantage later on, especially with espionage enabled if (otherCiv.cities.count { !it.getCenterTile().isVisible(civInfo) } < otherCiv.cities.count() * .8f) return true - if (hasAtLeastMotivationToAttack(civInfo, otherCiv, (diploManager.opinionOfOtherCiv() / 2).toInt()) > 0) + if (hasAtLeastMotivationToAttack(civInfo, otherCiv, + diploManager.opinionOfOtherCiv() * civInfo.getPersonality().modifierFocus(PersonalityValue.Commerce, .3f) / 2) > 0) return false return true } @@ -157,7 +165,7 @@ object DiplomacyAutomation { for (otherCiv in canSignResearchAgreementCiv) { // Default setting is 5, this will be changed according to different civ. - if ((1..10).random() <= 5) continue + if ((1..10).random() <= 5 * civInfo.getPersonality().modifierFocus(PersonalityValue.Science, .3f)) continue val tradeLogic = TradeLogic(civInfo, otherCiv) val cost = civInfo.diplomacyFunctions.getResearchAgreementCost(otherCiv) tradeLogic.currentTrade.ourOffers.add(TradeOffer(Constants.researchAgreement, TradeType.Treaty, cost)) @@ -174,13 +182,13 @@ object DiplomacyAutomation { .filter { civInfo.diplomacyFunctions.canSignDefensivePactWith(it) && !civInfo.getDiplomacyManager(it)!!.hasFlag(DiplomacyFlags.DeclinedDefensivePact) - && civInfo.getDiplomacyManager(it)!!.opinionOfOtherCiv() < 70f + && civInfo.getDiplomacyManager(it)!!.opinionOfOtherCiv() < 70f * civInfo.getPersonality().inverseModifierFocus(PersonalityValue.Aggressive, .2f) && !isTradeBeingOffered(civInfo, it, Constants.defensivePact) } for (otherCiv in canSignDefensivePactCiv) { // Default setting is 3, this will be changed according to different civ. - if ((1..10).random() <= 7) continue + if ((1..10).random() <= 7 * civInfo.getPersonality().inverseModifierFocus(PersonalityValue.Loyal, .3f)) continue if (wantsToSignDefensivePact(civInfo, otherCiv)) { //todo: Add more in depth evaluation here val tradeLogic = TradeLogic(civInfo, otherCiv) @@ -198,7 +206,7 @@ object DiplomacyAutomation { fun wantsToSignDefensivePact(civInfo: Civilization, otherCiv: Civilization): Boolean { val diploManager = civInfo.getDiplomacyManager(otherCiv)!! if (diploManager.hasFlag(DiplomacyFlags.DeclinedDefensivePact)) return false - if (diploManager.opinionOfOtherCiv() < 60f) return false + if (diploManager.opinionOfOtherCiv() < 65f * civInfo.getPersonality().inverseModifierFocus(PersonalityValue.Aggressive, .3f)) return false val commonknownCivs = diploManager.getCommonKnownCivs() for (thirdCiv in commonknownCivs) { // If they have bad relations with any of our friends, don't consider it @@ -222,28 +230,33 @@ object DiplomacyAutomation { val allAliveCivs = allCivs - deadCivs // We have to already be at RelationshipLevel.Ally, so we must have 80 oppinion of them - var motivation = diploManager.opinionOfOtherCiv().toInt() - 80 + var motivation = diploManager.opinionOfOtherCiv() - 80 + + // Warmongerers don't make good allies + if (diploManager.hasModifier(DiplomaticModifiers.WarMongerer)) { + motivation -= diploManager.getModifier(DiplomaticModifiers.WarMongerer) * civInfo.getPersonality().modifierFocus(PersonalityValue.Diplomacy, .5f) + } // If they are stronger than us, then we value it a lot more // If they are weaker than us, then we don't value it motivation += when (Automation.threatAssessment(civInfo, otherCiv)) { ThreatLevel.VeryHigh -> 10 ThreatLevel.High -> 5 - ThreatLevel.Low -> -5 - ThreatLevel.VeryLow -> -10 + ThreatLevel.Low -> -3 + ThreatLevel.VeryLow -> -7 else -> 0 } - // If they have a defensive pact with another civ then we would get drawn into thier battles as well + // If they have a defensive pact with another civ then we would get drawn into their battles as well motivation -= 15 * otherCivNonOverlappingDefensivePacts // Becomre more desperate as we have more wars motivation += civInfo.diplomacy.values.count { it.otherCiv().isMajorCiv() && it.diplomaticStatus == DiplomaticStatus.War } * 5 // Try to have a defensive pact with 1/5 of all civs - val civsToAllyWith = 0.20f * allAliveCivs - // Goes from 0 to -50 as the civ gets more allies, offset by civsToAllyWith - motivation -= (50f * (defensivePacts - civsToAllyWith) / (allAliveCivs - civsToAllyWith)).coerceAtMost(0f).toInt() + val civsToAllyWith = 0.20f * allAliveCivs * civInfo.getPersonality().modifierFocus(PersonalityValue.Diplomacy, .5f) + // Goes from 0 to -40 as the civ gets more allies, offset by civsToAllyWith + motivation -= (40f * (defensivePacts - civsToAllyWith) / (allAliveCivs - civsToAllyWith)).coerceAtMost(0f) return motivation > 0 } @@ -270,8 +283,8 @@ object DiplomacyAutomation { if (targetCivs.none()) return - val targetCivsWithMotivation: List> = targetCivs - .map { Pair(it, hasAtLeastMotivationToAttack(civInfo, it, 0)) } + val targetCivsWithMotivation: List> = targetCivs + .map { Pair(it, hasAtLeastMotivationToAttack(civInfo, it, 0f)) } .filter { it.second > 0 }.toList() DeclareWarTargetAutomation.chooseDeclareWarTarget(civInfo, targetCivsWithMotivation) @@ -293,7 +306,7 @@ object DiplomacyAutomation { .filter { it.tradeRequests.none { tradeRequest -> tradeRequest.requestingCiv == civInfo.civName && tradeRequest.trade.isPeaceTreaty() } } for (enemy in enemiesCiv) { - if (hasAtLeastMotivationToAttack(civInfo, enemy, 10) >= 10) { + if (hasAtLeastMotivationToAttack(civInfo, enemy, 10f) >= 10) { // We can still fight. Refuse peace. continue } @@ -336,7 +349,8 @@ object DiplomacyAutomation { internal fun askForHelp(civInfo: Civilization) { if (!civInfo.isAtWar() || civInfo.cities.isEmpty() || civInfo.diplomacy.isEmpty()) return - for (enemyCiv in civInfo.getCivsAtWarWith().sortedByDescending { it.getStatForRanking(RankingType.Force) }) { + val enemyCivs = civInfo.getCivsAtWarWith().filter { it.isMajorCiv() }.sortedByDescending { it.getStatForRanking(RankingType.Force) } + for (enemyCiv in enemyCivs) { val potentialAllies = enemyCiv.threatManager.getNeighboringCivilizations() .filter { civInfo.knows(it) && !it.isAtWarWith(enemyCiv) diff --git a/core/src/com/unciv/logic/automation/civilization/MotivationToAttackAutomation.kt b/core/src/com/unciv/logic/automation/civilization/MotivationToAttackAutomation.kt index b0f9ae0061..438e6f95c7 100644 --- a/core/src/com/unciv/logic/automation/civilization/MotivationToAttackAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/MotivationToAttackAutomation.kt @@ -12,6 +12,7 @@ import com.unciv.logic.map.BFS import com.unciv.logic.map.MapPathing import com.unciv.logic.map.tile.Tile import com.unciv.models.ruleset.Building +import com.unciv.models.ruleset.nation.PersonalityValue import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.ui.screens.victoryscreen.RankingType @@ -20,33 +21,38 @@ object MotivationToAttackAutomation { /** Will return the motivation to attack, but might short circuit if the value is guaranteed to * be lower than `atLeast`. So any values below `atLeast` should not be used for comparison. */ - fun hasAtLeastMotivationToAttack(civInfo: Civilization, targetCiv: Civilization, atLeast: Int): Int { + fun hasAtLeastMotivationToAttack(civInfo: Civilization, targetCiv: Civilization, atLeast: Float): Float { + val diplomacyManager = civInfo.getDiplomacyManager(targetCiv)!! + val personality = civInfo.getPersonality() + val targetCitiesWithOurCity = civInfo.threatManager.getNeighboringCitiesOfOtherCivs().filter { it.second.civ == targetCiv }.toList() val targetCities = targetCitiesWithOurCity.map { it.second } - if (targetCitiesWithOurCity.isEmpty()) return 0 + if (targetCitiesWithOurCity.isEmpty()) return 0f if (targetCities.all { hasNoUnitsThatCanAttackCityWithoutDying(civInfo, it) }) - return 0 + return 0f val baseForce = 100f val ourCombatStrength = calculateSelfCombatStrength(civInfo, baseForce) val theirCombatStrength = calculateCombatStrengthWithProtectors(targetCiv, baseForce, civInfo) - val modifiers:MutableList> = mutableListOf() - modifiers.add(Pair("Base motivation", -15)) + val modifiers: MutableList> = mutableListOf() - modifiers.add(Pair("Relative combat strength", getCombatStrengthModifier(ourCombatStrength, theirCombatStrength + 0.8f * civInfo.threatManager.getCombinedForceOfWarringCivs()))) + // If our personality is to declare war more then we should have a higher base motivation (a negative number closer to 0) + modifiers.add(Pair("Base motivation", -(15f * personality.inverseModifierFocus(PersonalityValue.DeclareWar, 0.5f)))) + + modifiers.add(Pair("Relative combat strength", getCombatStrengthModifier(civInfo, ourCombatStrength, theirCombatStrength + 0.8f * civInfo.threatManager.getCombinedForceOfWarringCivs()))) // TODO: For now this will be a very high value because the AI can't handle multiple fronts, this should be changed later though - modifiers.add(Pair("Concurrent wars", -civInfo.getCivsAtWarWith().count { it.isMajorCiv() && it != targetCiv } * 20)) - modifiers.add(Pair("Their concurrent wars", targetCiv.getCivsAtWarWith().count { it.isMajorCiv() } * 3)) + modifiers.add(Pair("Concurrent wars", -civInfo.getCivsAtWarWith().count { it.isMajorCiv() && it != targetCiv } * 20f)) + modifiers.add(Pair("Their concurrent wars", targetCiv.getCivsAtWarWith().count { it.isMajorCiv() } * 3f)) modifiers.add(Pair("Their allies", getDefensivePactAlliesScore(targetCiv, civInfo, baseForce, ourCombatStrength))) if (civInfo.threatManager.getNeighboringCivilizations().none { it != targetCiv && it.isMajorCiv() && civInfo.getDiplomacyManager(it)!!.isRelationshipLevelLT(RelationshipLevel.Friend) }) - modifiers.add(Pair("No other threats", 10)) + modifiers.add(Pair("No other threats", 10f)) if (targetCiv.isMajorCiv()) { val scoreRatioModifier = getScoreRatioModifier(targetCiv, civInfo) @@ -55,61 +61,65 @@ object MotivationToAttackAutomation { modifiers.add(Pair("Relative technologies", getRelativeTechModifier(civInfo, targetCiv))) if (civInfo.stats.getUnitSupplyDeficit() != 0) { - modifiers.add(Pair("Over unit supply", (civInfo.stats.getUnitSupplyDeficit() * 2).coerceAtMost(20))) + modifiers.add(Pair("Over unit supply", (civInfo.stats.getUnitSupplyDeficit() * 2f).coerceAtMost(20f))) } else if (targetCiv.stats.getUnitSupplyDeficit() == 0 && !targetCiv.isCityState()) { modifiers.add(Pair("Relative production", getProductionRatioModifier(civInfo, targetCiv))) } } val minTargetCityDistance = targetCitiesWithOurCity.minOf { it.second.getCenterTile().aerialDistanceTo(it.first.getCenterTile()) } + // Defensive civs should avoid fighting civilizations that are farther away and don't pose a threat modifiers.add(Pair("Far away cities", when { - minTargetCityDistance > 20 -> -10 - minTargetCityDistance > 14 -> -8 - minTargetCityDistance > 10 -> -3 - else -> 0 - })) - if (minTargetCityDistance < 6) modifiers.add(Pair("Close cities", 5)) + minTargetCityDistance > 20 -> -10f + minTargetCityDistance > 14 -> -8f + minTargetCityDistance > 10 -> -3f + else -> 0f + } * personality.inverseModifierFocus(PersonalityValue.Aggressive, 0.2f))) - val diplomacyManager = civInfo.getDiplomacyManager(targetCiv)!! + // Defensive civs want to deal with potential nearby cities to protect themselves + if (minTargetCityDistance < 6) + modifiers.add(Pair("Close cities", 5f * personality.inverseModifierFocus(PersonalityValue.Aggressive, 1f))) if (diplomacyManager.hasFlag(DiplomacyFlags.ResearchAgreement)) - modifiers.add(Pair("Research Agreement", -5)) + modifiers.add(Pair("Research Agreement", -5f * personality.modifierFocus(PersonalityValue.Loyal, .2f) + * personality.scaledFocus(PersonalityValue.Science) * personality.modifierFocus(PersonalityValue.Commerce, .3f))) if (diplomacyManager.hasFlag(DiplomacyFlags.DeclarationOfFriendship)) - modifiers.add(Pair("Declaration of Friendship", -10)) + modifiers.add(Pair("Declaration of Friendship", -10f * personality.modifierFocus(PersonalityValue.Loyal, .5f))) if (diplomacyManager.hasFlag(DiplomacyFlags.DefensivePact)) - modifiers.add(Pair("Defensive Pact", -15)) + modifiers.add(Pair("Defensive Pact", -15f * personality.modifierFocus(PersonalityValue.Loyal, .3f))) modifiers.add(Pair("Relationship", getRelationshipModifier(diplomacyManager))) if (diplomacyManager.hasFlag(DiplomacyFlags.Denunciation)) { - modifiers.add(Pair("Denunciation", 5)) + modifiers.add(Pair("Denunciation", 5f * personality.inverseModifierFocus(PersonalityValue.Diplomacy, .5f))) } if (diplomacyManager.hasFlag(DiplomacyFlags.WaryOf) && diplomacyManager.getFlag(DiplomacyFlags.WaryOf) < 0) { - modifiers.add(Pair("PlanningAttack", -diplomacyManager.getFlag(DiplomacyFlags.WaryOf))) + // Completely defensive civs will plan defensively and have a 0 here + modifiers.add(Pair("PlanningAttack", -diplomacyManager.getFlag(DiplomacyFlags.WaryOf) * personality.scaledFocus(PersonalityValue.Aggressive) / 2)) } else { val attacksPlanned = civInfo.diplomacy.values.count { it.hasFlag(DiplomacyFlags.WaryOf) && it.getFlag(DiplomacyFlags.WaryOf) < 0 } - modifiers.add(Pair("PlanningAttackAgainstOtherCivs", -attacksPlanned * 5)) + modifiers.add(Pair("PlanningAttackAgainstOtherCivs", -attacksPlanned * 5f * personality.inverseModifierFocus(PersonalityValue.Aggressive, .5f))) } if (diplomacyManager.resourcesFromTrade().any { it.amount > 0 }) - modifiers.add(Pair("Receiving trade resources", -5)) + modifiers.add(Pair("Receiving trade resources", -8f * personality.modifierFocus(PersonalityValue.Commerce, .5f) * personality.modifierFocus(PersonalityValue.Loyal, .2f))) // If their cities don't have any nearby cities that are also targets to us and it doesn't include their capital // Then there cities are likely isolated and a good target. if (targetCiv.getCapital(true) !in targetCities && targetCities.all { theirCity -> !theirCity.neighboringCities.any { it !in targetCities } }) { - modifiers.add(Pair("Isolated city", 15)) + modifiers.add(Pair("Isolated city", 10f * personality.modifierFocus(PersonalityValue.Aggressive, .8f))) } if (targetCiv.isCityState()) { - modifiers.add(Pair("Protectors", -targetCiv.cityStateFunctions.getProtectorCivs().size * 3)) + modifiers.add(Pair("Protectors", -targetCiv.cityStateFunctions.getProtectorCivs().size * 3f)) if (targetCiv.cityStateFunctions.getProtectorCivs().contains(civInfo)) - modifiers.add(Pair("Under our protection", -15)) + modifiers.add(Pair("Under our protection", -15 * personality.modifierFocus(PersonalityValue.Loyal, .8f))) if (targetCiv.getAllyCiv() == civInfo.civName) - modifiers.add(Pair("Allied City-state", -20)) // There had better be a DAMN good reason + modifiers.add(Pair("Allied City-state", -20 * personality.modifierFocus(PersonalityValue.Loyal, .8f))) // There had better be a DAMN good reason } addWonderBasedMotivations(targetCiv, modifiers) @@ -117,8 +127,8 @@ object MotivationToAttackAutomation { modifiers.add(Pair("War with allies", getAlliedWarMotivation(civInfo, targetCiv))) // Purely for debugging, remove modifiers that don't have an effect - modifiers.removeAll { it.second == 0 } - var motivationSoFar = modifiers.sumOf { it.second } + modifiers.removeAll { it.second == 0f } + var motivationSoFar = modifiers.map { it.second }.sum() // Short-circuit to avoid A-star if (motivationSoFar < atLeast) return motivationSoFar @@ -145,69 +155,70 @@ object MotivationToAttackAutomation { return ourCombatStrength } - private fun addWonderBasedMotivations(otherCiv: Civilization, modifiers: MutableList>) { + private fun addWonderBasedMotivations(otherCiv: Civilization, modifiers: MutableList>) { var wonderCount = 0 for (city in otherCiv.cities) { val construction = city.cityConstructions.getCurrentConstruction() if (construction is Building && construction.hasUnique(UniqueType.TriggersCulturalVictory)) - modifiers.add(Pair("About to win", 15)) + modifiers.add(Pair("About to win", 15f)) if (construction is BaseUnit && construction.hasUnique(UniqueType.AddInCapital)) - modifiers.add(Pair("About to win", 15)) + modifiers.add(Pair("About to win", 15f)) wonderCount += city.cityConstructions.getBuiltBuildings().count { it.isWonder } } // The more wonders they have, the more beneficial it is to conquer them // Civs need an army to protect thier wonders which give the most score if (wonderCount > 0) - modifiers.add(Pair("Owned Wonders", wonderCount)) + modifiers.add(Pair("Owned Wonders", wonderCount.toFloat())) } /** If they are at war with our allies, then we should join in */ - private fun getAlliedWarMotivation(civInfo: Civilization, otherCiv: Civilization): Int { - var alliedWarMotivation = 0 + private fun getAlliedWarMotivation(civInfo: Civilization, otherCiv: Civilization): Float { + var alliedWarMotivation = 0f for (thirdCiv in civInfo.getDiplomacyManager(otherCiv)!!.getCommonKnownCivs()) { val thirdCivDiploManager = civInfo.getDiplomacyManager(thirdCiv) - if (thirdCivDiploManager!!.isRelationshipLevelGE(RelationshipLevel.Friend) - && thirdCiv.isAtWarWith(otherCiv) - ) { - if (thirdCiv.getDiplomacyManager(otherCiv)!!.hasFlag(DiplomacyFlags.Denunciation)) - alliedWarMotivation += 2 - alliedWarMotivation += if (thirdCivDiploManager.hasFlag(DiplomacyFlags.DefensivePact)) 15 - else if (thirdCivDiploManager.isRelationshipLevelGT(RelationshipLevel.Friend)) 5 - else 2 + if (thirdCivDiploManager!!.isRelationshipLevelLT(RelationshipLevel.Friend)) continue + + if (thirdCiv.getDiplomacyManager(otherCiv)!!.hasFlag(DiplomacyFlags.Denunciation)) + alliedWarMotivation += 2f + + if (thirdCiv.isAtWarWith(otherCiv)) { + alliedWarMotivation += if (thirdCivDiploManager.hasFlag(DiplomacyFlags.DefensivePact)) 15f + else if (thirdCivDiploManager.hasFlag(DiplomacyFlags.DeclarationOfFriendship)) 5f + else 2f } } - return alliedWarMotivation + return alliedWarMotivation * civInfo.getPersonality().modifierFocus(PersonalityValue.Loyal, .5f) } - private fun getRelationshipModifier(diplomacyManager: DiplomacyManager): Int { + private fun getRelationshipModifier(diplomacyManager: DiplomacyManager): Float { val relationshipModifier = when (diplomacyManager.relationshipIgnoreAfraid()) { - RelationshipLevel.Unforgivable -> 15 - RelationshipLevel.Enemy -> 10 - RelationshipLevel.Competitor -> 5 - RelationshipLevel.Favorable -> -2 - RelationshipLevel.Friend -> -5 - RelationshipLevel.Ally -> -10 // this is so that ally + DoF is not too unbalanced - + RelationshipLevel.Unforgivable -> 15f + RelationshipLevel.Enemy -> 10f + RelationshipLevel.Competitor -> 5f + RelationshipLevel.Favorable -> -2f + RelationshipLevel.Friend -> -5f + RelationshipLevel.Ally -> -10f // this is so that ally + DoF is not too unbalanced - // still possible for AI to declare war for isolated city - else -> 0 + else -> 0f } - return relationshipModifier + return relationshipModifier * diplomacyManager.civInfo.getPersonality().modifierFocus(PersonalityValue.Diplomacy, .3f) } - private fun getRelativeTechModifier(civInfo: Civilization, otherCiv: Civilization): Int { + private fun getRelativeTechModifier(civInfo: Civilization, otherCiv: Civilization): Float { val relativeTech = civInfo.getStatForRanking(RankingType.Technologies) - otherCiv.getStatForRanking(RankingType.Technologies) val relativeTechModifier = when { - relativeTech > 6 -> 10 - relativeTech > 3 -> 5 - relativeTech > -3 -> 0 - relativeTech > -6 -> -2 - relativeTech > -9 -> -5 - else -> -10 + relativeTech > 6 -> 10f + relativeTech > 3 -> 5f + relativeTech > -3 -> 0f + relativeTech > -6 -> -2f + relativeTech > -9 -> -5f + else -> -10f } - return relativeTechModifier + return relativeTechModifier * civInfo.getPersonality().modifierFocus(PersonalityValue.Science, .2f) } - private fun getProductionRatioModifier(civInfo: Civilization, otherCiv: Civilization): Int { + private fun getProductionRatioModifier(civInfo: Civilization, otherCiv: Civilization): Float { // If either of our Civs are suffering from a supply deficit, our army must be too large // There is no easy way to check the raw production if a civ has a supply deficit // We might try to divide the current production by the getUnitSupplyProductionPenalty() @@ -215,67 +226,69 @@ object MotivationToAttackAutomation { val productionRatio = civInfo.getStatForRanking(RankingType.Production).toFloat() / otherCiv.getStatForRanking(RankingType.Production).toFloat() val productionRatioModifier = when { - productionRatio > 2f -> 15 - productionRatio > 1.5f -> 7 - productionRatio > 1.2 -> 3 - productionRatio > .8f -> 0 - productionRatio > .5f -> -5 - productionRatio > .25f -> -10 - else -> -10 + productionRatio > 2f -> 15f + productionRatio > 1.5f -> 7f + productionRatio > 1.2 -> 3f + productionRatio > .8f -> 0f + productionRatio > .5f -> -5f + productionRatio > .25f -> -10f + else -> -10f } - return productionRatioModifier + return productionRatioModifier * civInfo.getPersonality().modifierFocus(PersonalityValue.Production, .2f) } - private fun getScoreRatioModifier(otherCiv: Civilization, civInfo: Civilization): Int { + private fun getScoreRatioModifier(otherCiv: Civilization, civInfo: Civilization): Float { // Civs with more score are more threatening to our victory // Bias towards attacking civs with a high score and low military // Bias against attacking civs with a low score and a high military // Designed to mitigate AIs declaring war on weaker civs instead of their rivals val scoreRatio = otherCiv.getStatForRanking(RankingType.Score).toFloat() / civInfo.getStatForRanking(RankingType.Score).toFloat() val scoreRatioModifier = when { - scoreRatio > 2f -> 20 - scoreRatio > 1.5f -> 15 - scoreRatio > 1.25f -> 10 - scoreRatio > 1f -> 2 - scoreRatio > .8f -> 0 - scoreRatio > .5f -> -2 - scoreRatio > .25f -> -5 - else -> -10 + scoreRatio > 2f -> 15f + scoreRatio > 1.5f -> 10f + scoreRatio > 1.25f -> 5f + scoreRatio > 1f -> 2f + scoreRatio > .8f -> 0f + scoreRatio > .5f -> -2f + scoreRatio > .25f -> -5f + else -> -10f } return scoreRatioModifier } - private fun getDefensivePactAlliesScore(otherCiv: Civilization, civInfo: Civilization, baseForce: Float, ourCombatStrength: Float): Int { - var theirAlliesValue = 0 + private fun getDefensivePactAlliesScore(otherCiv: Civilization, civInfo: Civilization, baseForce: Float, ourCombatStrength: Float): Float { + var theirAlliesValue = 0f for (thirdCiv in otherCiv.diplomacy.values.filter { it.hasFlag(DiplomacyFlags.DefensivePact) && it.otherCiv() != civInfo }) { val thirdCivCombatStrengthRatio = otherCiv.getStatForRanking(RankingType.Force).toFloat() + baseForce / ourCombatStrength theirAlliesValue += when { - thirdCivCombatStrengthRatio > 5 -> -15 - thirdCivCombatStrengthRatio > 2.5 -> -10 - thirdCivCombatStrengthRatio > 2 -> -8 - thirdCivCombatStrengthRatio > 1.5 -> -5 - thirdCivCombatStrengthRatio > .8 -> -2 - else -> 0 + thirdCivCombatStrengthRatio > 5 -> -15f + thirdCivCombatStrengthRatio > 2.5 -> -10f + thirdCivCombatStrengthRatio > 2 -> -8f + thirdCivCombatStrengthRatio > 1.5 -> -5f + thirdCivCombatStrengthRatio > .8 -> -2f + else -> 0f } } return theirAlliesValue } - private fun getCombatStrengthModifier(ourCombatStrength: Float, theirCombatStrength: Float): Int { + private fun getCombatStrengthModifier(civInfo: Civilization, ourCombatStrength: Float, theirCombatStrength: Float): Float { val combatStrengthRatio = ourCombatStrength / theirCombatStrength val combatStrengthModifier = when { - combatStrengthRatio > 5f -> 30 - combatStrengthRatio > 4f -> 20 - combatStrengthRatio > 3f -> 15 - combatStrengthRatio > 2f -> 10 - combatStrengthRatio > 1.5f -> 5 - combatStrengthRatio > .8f -> 0 - combatStrengthRatio > .6f -> -5 - combatStrengthRatio > .4f -> -15 - combatStrengthRatio > .2f -> -20 - else -> -20 + combatStrengthRatio > 5f -> 20f + combatStrengthRatio > 4f -> 15f + combatStrengthRatio > 3f -> 12f + combatStrengthRatio > 2f -> 10f + combatStrengthRatio > 1.8f -> 8f + combatStrengthRatio > 1.6f -> 6f + combatStrengthRatio > 1.4f -> 4f + combatStrengthRatio > 1.2f -> 2f + combatStrengthRatio > .8f -> 0f + combatStrengthRatio > .6f -> -5f + combatStrengthRatio > .4f -> -15f + else -> -20f } - return combatStrengthModifier + return combatStrengthModifier * civInfo.getPersonality().modifierFocus(PersonalityValue.Military, .2f) } private fun hasNoUnitsThatCanAttackCityWithoutDying(civInfo: Civilization, theirCity: City) = civInfo.units.getCivUnits().filter { it.isMilitary() }.none { @@ -295,7 +308,7 @@ object MotivationToAttackAutomation { * * @return The motivation ranging from -30 to around +10 */ - private fun getAttackPathsModifier(civInfo: Civilization, otherCiv: Civilization, targetCitiesWithOurCity: List>): Int { + private fun getAttackPathsModifier(civInfo: Civilization, otherCiv: Civilization, targetCitiesWithOurCity: List>): Float { fun isTileCanMoveThrough(civInfo: Civilization, tile: Tile): Boolean { val owner = tile.getOwner() @@ -308,12 +321,12 @@ object MotivationToAttackAutomation { } val attackPaths: MutableList> = mutableListOf() - var attackPathModifiers: Int = -3 + var attackPathModifiers: Float = -3f // For each city, we want to calculate if there is an attack path to the enemy for (attacksGroupedByCity in targetCitiesWithOurCity.groupBy { it.first }) { val cityToAttackFrom = attacksGroupedByCity.key - var cityAttackValue = 0 + var cityAttackValue = 0f // We only want to calculate the best attack path and use it's value // Land routes are clearly better than sea routes @@ -321,7 +334,7 @@ object MotivationToAttackAutomation { val landAttackPath = MapPathing.getConnection(civInfo, cityToAttackFrom.getCenterTile(), cityToAttack.getCenterTile(), ::isLandTileCanMoveThrough) if (landAttackPath != null && landAttackPath.size < 16) { attackPaths.add(landAttackPath) - cityAttackValue = 3 + cityAttackValue = 3f break } @@ -341,7 +354,7 @@ object MotivationToAttackAutomation { val reachableEnemyCitiesBfs = BFS(civInfo.getCapital(true)!!.getCenterTile()) { isTileCanMoveThrough(civInfo, it) } reachableEnemyCitiesBfs.stepToEnd() val reachableEnemyCities = otherCiv.cities.filter { reachableEnemyCitiesBfs.hasReachedTile(it.getCenterTile()) } - if (reachableEnemyCities.isEmpty()) return -50 // Can't even reach the enemy city, no point in war. + if (reachableEnemyCities.isEmpty()) return -50f // Can't even reach the enemy city, no point in war. val minAttackDistance = reachableEnemyCities.minOf { reachableEnemyCitiesBfs.getPathTo(it.getCenterTile()).count() } // Longer attack paths are worse, but if the attack path is too far away we shouldn't completely discard the possibility diff --git a/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt index a57e030bae..dd7e96231f 100644 --- a/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt @@ -468,19 +468,18 @@ object NextTurnAutomation { if (civInfo.cities.none()) return if (civInfo.getHappiness() <= civInfo.cities.size) return + if (civInfo.units.getCivUnits().any { it.hasUnique(UniqueType.FoundCity) }) return + if (civInfo.cities.any { + val currentConstruction = it.cityConstructions.getCurrentConstruction() + currentConstruction is BaseUnit && currentConstruction.isCityFounder() + }) return val settlerUnits = civInfo.gameInfo.ruleset.units.values .filter { it.isCityFounder() && it.isBuildable(civInfo) && personality.getMatchingUniques(UniqueType.WillNotBuild, StateForConditionals(civInfo)) .none { unique -> it.matchesFilter(unique.params[0]) } } if (settlerUnits.isEmpty()) return - if (!civInfo.units.getCivUnits().none { it.hasUnique(UniqueType.FoundCity) }) return - if (civInfo.cities.any { - val currentConstruction = it.cityConstructions.getCurrentConstruction() - currentConstruction is BaseUnit && currentConstruction.isCityFounder() - }) return - - if (civInfo.units.getCivUnits().none { it.isMilitary() }) return // We need someone to defend him first + if (civInfo.units.getCivUnits().count { it.isMilitary() } < civInfo.cities.size) return // We need someone to defend them first val workersBuildableForThisCiv = civInfo.gameInfo.ruleset.units.values.any { it.hasUnique(UniqueType.BuildImprovements) diff --git a/core/src/com/unciv/logic/automation/civilization/UseGoldAutomation.kt b/core/src/com/unciv/logic/automation/civilization/UseGoldAutomation.kt index 9704cb6581..44a9ec59e6 100644 --- a/core/src/com/unciv/logic/automation/civilization/UseGoldAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/UseGoldAutomation.kt @@ -37,7 +37,7 @@ object UseGoldAutomation { private fun useGoldForCityStates(civ: Civilization) { // RARE EDGE CASE: If you ally with a city-state, you may reveal more map that includes ANOTHER civ! // So if we don't lock this list, we may later discover that there are more known civs, concurrent modification exception! - val knownCityStates = civ.getKnownCivs().filter { it.isCityState() && MotivationToAttackAutomation.hasAtLeastMotivationToAttack(civ, it, 0) <= 0 }.toList() + val knownCityStates = civ.getKnownCivs().filter { it.isCityState() && MotivationToAttackAutomation.hasAtLeastMotivationToAttack(civ, it, 0f) <= 0 }.toList() // canBeMarriedBy checks actual cost, but it can't be below 500*speedmodifier, and the later check is expensive if (civ.gold >= 330 && civ.getHappiness() > 0 && civ.hasUnique(UniqueType.CityStateCanBeBoughtForGold)) { diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DeclareWar.kt b/core/src/com/unciv/logic/civilization/diplomacy/DeclareWar.kt index 7262b20777..d9ee6fa4a3 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/DeclareWar.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/DeclareWar.kt @@ -7,6 +7,7 @@ import com.unciv.logic.civilization.DiplomacyAction import com.unciv.logic.civilization.NotificationCategory import com.unciv.logic.civilization.NotificationIcon import com.unciv.logic.civilization.PopupAlert +import com.unciv.models.ruleset.nation.PersonalityValue import com.unciv.models.ruleset.unique.UniqueTriggerActivation import com.unciv.models.ruleset.unique.UniqueType @@ -186,11 +187,11 @@ object DeclareWar { if (thirdCiv.isAtWarWith(otherCiv) && !thirdCiv.isAtWarWith(civInfo)) { // Improve our relations if (thirdCiv.isCityState()) thirdCiv.getDiplomacyManager(civInfo)!!.addInfluence(10f) - else thirdCiv.getDiplomacyManager(civInfo)!!.addModifier(DiplomaticModifiers.SharedEnemy, 5f) + else thirdCiv.getDiplomacyManager(civInfo)!!.addModifier(DiplomaticModifiers.SharedEnemy, 5f * civInfo.getPersonality().modifierFocus(PersonalityValue.Loyal, .3f)) } else if (thirdCiv.isAtWarWith(civInfo)) { // Improve their relations if (thirdCiv.isCityState()) thirdCiv.getDiplomacyManager(otherCiv)!!.addInfluence(10f) - else thirdCiv.getDiplomacyManager(otherCiv)!!.addModifier(DiplomaticModifiers.SharedEnemy, 5f) + else thirdCiv.getDiplomacyManager(otherCiv)!!.addModifier(DiplomaticModifiers.SharedEnemy, 5f * civInfo.getPersonality().modifierFocus(PersonalityValue.Loyal, .3f)) } } } @@ -220,12 +221,12 @@ object DeclareWar { val diploManager = knownCiv.getDiplomacyManager(diplomacyManager.civInfo)!! if (betrayedFriendship) { val amount = if (knownCiv == otherCiv) -40f else -20f - diploManager.addModifier(DiplomaticModifiers.BetrayedDeclarationOfFriendship, amount) + diploManager.addModifier(DiplomaticModifiers.BetrayedDeclarationOfFriendship, amount * knownCiv.getPersonality().modifierFocus(PersonalityValue.Loyal, .3f)) } if (betrayedDefensivePact) { //Note: this stacks with Declaration of Friendship val amount = if (knownCiv == otherCiv) -20f else -10f - diploManager.addModifier(DiplomaticModifiers.BetrayedDefensivePact, amount) + diploManager.addModifier(DiplomaticModifiers.BetrayedDefensivePact, amount * knownCiv.getPersonality().modifierFocus(PersonalityValue.Loyal, .3f)) } diploManager.removeModifier(DiplomaticModifiers.DeclaredFriendshipWithOurAllies) // obviously this guy's declarations of friendship aren't worth much. diploManager.removeModifier(DiplomaticModifiers.SignedDefensivePactWithOurAllies) diff --git a/core/src/com/unciv/logic/trade/Trade.kt b/core/src/com/unciv/logic/trade/Trade.kt index 55c6c8aff2..7594fe47b2 100644 --- a/core/src/com/unciv/logic/trade/Trade.kt +++ b/core/src/com/unciv/logic/trade/Trade.kt @@ -6,6 +6,7 @@ import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.NotificationCategory import com.unciv.logic.civilization.NotificationIcon import com.unciv.logic.civilization.diplomacy.DiplomacyFlags +import com.unciv.models.ruleset.nation.PersonalityValue class Trade : IsPartOfGameInfoSerialization { @@ -57,9 +58,9 @@ class TradeRequest : IsPartOfGameInfoSerialization { // the numbers of the flags (20,5) are the amount of turns to wait until offering again if (trade.ourOffers.all { it.type == TradeType.Luxury_Resource } && trade.theirOffers.all { it.type == TradeType.Luxury_Resource }) - requestingCivDiploManager.setFlag(DiplomacyFlags.DeclinedLuxExchange,5) + requestingCivDiploManager.setFlag(DiplomacyFlags.DeclinedLuxExchange,5 - (requestingCivInfo.getPersonality()[PersonalityValue.Commerce] / 2).toInt()) if (trade.ourOffers.any { it.name == Constants.researchAgreement }) - requestingCivDiploManager.setFlag(DiplomacyFlags.DeclinedResearchAgreement,10) + requestingCivDiploManager.setFlag(DiplomacyFlags.DeclinedResearchAgreement,15 - requestingCivInfo.getPersonality()[PersonalityValue.Science].toInt()) if (trade.ourOffers.any { it.name == Constants.defensivePact }) requestingCivDiploManager.setFlag(DiplomacyFlags.DeclinedDefensivePact,10) if (trade.ourOffers.any { it.name == Constants.openBorders }) diff --git a/core/src/com/unciv/logic/trade/TradeEvaluation.kt b/core/src/com/unciv/logic/trade/TradeEvaluation.kt index e185466859..aca6979fb5 100644 --- a/core/src/com/unciv/logic/trade/TradeEvaluation.kt +++ b/core/src/com/unciv/logic/trade/TradeEvaluation.kt @@ -163,7 +163,7 @@ class TradeEvaluation { return 0 } else if (civInfo.isAtWarWith(civToDeclareWarOn)) { // We shouldn't require them to pay us to join our war (no negative values) - return (20 * DeclareWarPlanEvaluator.evaluateJoinOurWarPlan(civInfo, civToDeclareWarOn, tradePartner, null)).coerceAtLeast(0) + return (20 * DeclareWarPlanEvaluator.evaluateJoinOurWarPlan(civInfo, civToDeclareWarOn, tradePartner, null)).toInt().coerceAtLeast(0) } else { // Why should we pay you to go fight someone else? return 0 @@ -281,13 +281,13 @@ class TradeEvaluation { && trade.ourOffers.any {it.type == TradeType.WarDeclaration && it.name == offer.name}) { // Only accept if the war will benefit us, or if they pay us enough // We shouldn't want to pay them for us to declare war (no negative values) - return (-20 * DeclareWarPlanEvaluator.evaluateTeamWarPlan(civInfo, civToDeclareWarOn, tradePartner, null)).coerceAtLeast(0) + return (-20 * DeclareWarPlanEvaluator.evaluateTeamWarPlan(civInfo, civToDeclareWarOn, tradePartner, null)).toInt().coerceAtLeast(0) } else if (tradePartner.isAtWarWith(civToDeclareWarOn)) { // We might want them to pay us to join them in war (no negative values) - return (-20 * DeclareWarPlanEvaluator.evaluateJoinWarPlan(civInfo, civToDeclareWarOn, tradePartner, null)).coerceAtLeast(0) + return (-20 * DeclareWarPlanEvaluator.evaluateJoinWarPlan(civInfo, civToDeclareWarOn, tradePartner, null)).toInt().coerceAtLeast(0) } else { // We might want them to pay us to declare war (no negative values) - return (-25 * DeclareWarPlanEvaluator.evaluateDeclareWarPlan(civInfo, civToDeclareWarOn, null)).coerceAtLeast(0) + return (-25 * DeclareWarPlanEvaluator.evaluateDeclareWarPlan(civInfo, civToDeclareWarOn, null)).toInt().coerceAtLeast(0) } } @@ -347,7 +347,7 @@ class TradeEvaluation { if (ourCombatStrength * 1.5f >= theirCombatStrength && theirCombatStrength * 1.5f >= ourCombatStrength) return 0 // we're roughly equal, there's no huge power imbalance if (ourCombatStrength > theirCombatStrength) { - if (MotivationToAttackAutomation.hasAtLeastMotivationToAttack(ourCiv, otherCiv, 0) <= 0) return 0 + if (MotivationToAttackAutomation.hasAtLeastMotivationToAttack(ourCiv, otherCiv, 0f) <= 0) return 0 val absoluteAdvantage = ourCombatStrength - theirCombatStrength val percentageAdvantage = absoluteAdvantage / theirCombatStrength.toFloat() // We don't add the same constraint here. We should not make peace easily if we're diff --git a/core/src/com/unciv/models/ruleset/nation/Personality.kt b/core/src/com/unciv/models/ruleset/nation/Personality.kt index c523e4edb6..6df299bb67 100644 --- a/core/src/com/unciv/models/ruleset/nation/Personality.kt +++ b/core/src/com/unciv/models/ruleset/nation/Personality.kt @@ -93,12 +93,19 @@ class Personality: RulesetObject() { } /** - * Scales the value to a more meaningful range, where 10 is 2, and 5 is 1 + * Scales the value to a more meaningful range, where 10 is 2, and 5 is 1, and 0 is 0 */ fun scaledFocus(value: PersonalityValue): Float { return nameToVariable(value).get() / 5 } + /** + * Inverse scales the value to a more meaningful range, where 0 is 2, and 5 is 1 and 10 is 0 + */ + fun inverseScaledFocus(value: PersonalityValue): Float { + return (10 - nameToVariable(value).get()) / 5 + } + /** * @param weight a value between 0 and 1 that determines how much the modifier deviates from 1 * @return a modifier between 0 and 2 centered around 1 based off of the personality value and the weight given @@ -107,6 +114,15 @@ class Personality: RulesetObject() { return 1f + (scaledFocus(value) - 1) * weight } + /** + * An inverted version of [modifierFocus], a personality value of 0 becomes a 10, 8 becomes a 2, etc. + * @param weight a value between 0 and 1 that determines how much the modifier deviates from 1 + * @return a modifier between 0 and 2 centered around 1 based off of the personality value and the weight given + */ + fun inverseModifierFocus(value: PersonalityValue, weight: Float): Float { + return 1f - (inverseScaledFocus(value) - 2) * weight + } + /** * Scales the stats based on the personality and the weight given * @param weight a positive value that determines how much the personality should impact the stats given