From a01a6270fc64e5d889f59cc5988d7d563909d55b Mon Sep 17 00:00:00 2001 From: SimonCeder <63475501+SimonCeder@users.noreply.github.com> Date: Mon, 30 Aug 2021 13:17:20 +0200 Subject: [PATCH] Demanding tribute from city states (#4976) * tribute willingness calculations * implement demanding gold and workers * Revisions * unit power calculation * show modifiers in the diplo screen * template.properties * G&K modifiers * promotions start at 0 * notiifications, AI * conflict * conflict? * template and translation, failing test? * another missing string * missing space * afraid relationship status * missing space 2 * Slight optimization * optimization pt 2 * reviews --- .../jsons/translations/Swedish.properties | 23 +++ .../jsons/translations/template.properties | 22 +++ .../logic/automation/NextTurnAutomation.kt | 86 ++++++++-- core/src/com/unciv/logic/city/CityInfo.kt | 13 +- .../logic/civilization/CivilizationInfo.kt | 159 ++++++++++++++++++ .../diplomacy/DiplomacyManager.kt | 15 +- core/src/com/unciv/logic/map/MapUnit.kt | 9 + .../com/unciv/logic/trade/TradeEvaluation.kt | 2 +- .../com/unciv/models/ruleset/unit/BaseUnit.kt | 78 +++++++++ .../src/com/unciv/ui/tilegroups/CityButton.kt | 1 + .../src/com/unciv/ui/trade/DiplomacyScreen.kt | 53 ++++++ 11 files changed, 438 insertions(+), 23 deletions(-) diff --git a/android/assets/jsons/translations/Swedish.properties b/android/assets/jsons/translations/Swedish.properties index 3f926d8e1f..fe6e170d0e 100644 --- a/android/assets/jsons/translations/Swedish.properties +++ b/android/assets/jsons/translations/Swedish.properties @@ -104,6 +104,7 @@ We promised not to settle near them ([count] turns remaining) = Vi lovade att in They promised not to settle near us ([count] turns remaining) = De lovade att inte bygga städer nära oss ([count] drag kvar) Unforgivable = Oförlåtlig +Afraid = Rädd Enemy = Fiende Competitor = Konkurrent Neutral = Neutral @@ -173,6 +174,7 @@ Personality = Personlighet Influence = Inflytande Reach 30 for friendship. = Nå 30 för vänskap. Reach highest influence above 60 for alliance. = Nå högsta inflytande över 60 för allians. + # Requires translation! When Friends: = # Requires translation! @@ -180,6 +182,27 @@ When Allies: = # Requires translation! The unique luxury is one of: = +Demand Tribute = Kräv Brandskatt +Tribute Willingness = Brandskattsvillighet +>0 to take gold, >30 and size 4 city for worker = >0 för att ta guld, >30 och stad av storlek 4 för arbetare +Major Civ = Stor Civilisation +No Cities = Inga Städer +Base value = Grundvärde +Has Ally = Har Allierad +Has Protector = Har Beskyddare +Demanding a Worker = Kräver Arbetare +Demanding a Worker from small City-State = Kräver Arbetare från liten Stadsstat +Very recently paid tribute = Alldeles nyss brandskattad +Recently paid tribute = Nyligen brandskattad +Influence below -30 = Inflytande under -30 +Military Rank = Militär Rangordning +Military near City-State = Militär nära Stadsstaten +Sum: = Summa: +Take [amount] gold (-15 Influence) = Ta [amount] guld (-15 Inflytande) +Take worker (-50 Influence) = Ta arbetare (-50 Inflytande) +[civName] is afraid of your military power! = [civName] är rädd för din militära makt! + + # Trades Trade = Handelsavtal diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 5a0ba71418..ad3273183e 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -104,6 +104,7 @@ We promised not to settle near them ([count] turns remaining) = They promised not to settle near us ([count] turns remaining) = Unforgivable = +Afraid = Enemy = Competitor = Neutral = @@ -178,6 +179,27 @@ When Friends: = When Allies: = The unique luxury is one of: = +Demand Tribute = +Tribute Willingness = +>0 to take gold, >30 and size 4 city for worker = +Major Civ = +No Cities = +Base value = +Has Ally = +Has Protector = +Demanding a Worker = +Demanding a Worker from small City-State = +Very recently paid tribute = +Recently paid tribute = +Influence below -30 = +Military Rank = +Military near City-State = +Sum: = +Take [amount] gold (-15 Influence) = +Take worker (-50 Influence) = +[civName] is afraid of your military power! = + + # Trades Trade = diff --git a/core/src/com/unciv/logic/automation/NextTurnAutomation.kt b/core/src/com/unciv/logic/automation/NextTurnAutomation.kt index efb1c896fc..488fc5e9ab 100644 --- a/core/src/com/unciv/logic/automation/NextTurnAutomation.kt +++ b/core/src/com/unciv/logic/automation/NextTurnAutomation.kt @@ -16,6 +16,7 @@ import com.unciv.logic.trade.* import com.unciv.models.ruleset.ModOptionsConstants import com.unciv.models.ruleset.VictoryType import com.unciv.models.ruleset.tech.Technology +import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.stats.Stat import com.unciv.models.translations.tr @@ -49,7 +50,10 @@ object NextTurnAutomation { chooseTechToResearch(civInfo) automateCityBombardment(civInfo) useGold(civInfo) - protectCityStates(civInfo) + if (!civInfo.isCityState()) { + protectCityStates(civInfo) + bullyCityStates(civInfo) + } automateUnits(civInfo) // this is the most expensive part reassignWorkedTiles(civInfo) // second most expensive trainSettler(civInfo) @@ -114,29 +118,19 @@ object NextTurnAutomation { /** allow AI to spend money to purchase city-state friendship, buildings & unit */ private fun useGold(civInfo: CivilizationInfo) { - if (civInfo.victoryType() == VictoryType.Cultural) { - for (cityState in civInfo.getKnownCivs() - .filter { it.isCityState() && it.cityStateType == CityStateType.Cultured }) { - val diploManager = cityState.getDiplomacyManager(civInfo) - if (diploManager.influence < 40) { // we want to gain influence with them + + if (!civInfo.isCityState()) { + val potentialAllies = civInfo.getKnownCivs().filter { it.isCityState() } + if (potentialAllies.isNotEmpty()) { + val cityState = + potentialAllies.maxByOrNull { valueCityStateAlliance(civInfo, it) }!! + if (cityState.getAllyCiv() != civInfo.civName && valueCityStateAlliance(civInfo, cityState) > 0) { tryGainInfluence(civInfo, cityState) return } } } - if (civInfo.getHappiness() < 5) { - for (cityState in civInfo.getKnownCivs() - .filter { it.isCityState() && it.cityStateType == CityStateType.Mercantile }) { - val diploManager = cityState.getDiplomacyManager(civInfo) - if (diploManager.influence < 40) { // we want to gain influence with them - tryGainInfluence(civInfo, cityState) - return - } - } - } - - for (city in civInfo.cities.sortedByDescending { it.population.population }) { val construction = city.cityConstructions.getCurrentConstruction() if (construction is PerpetualConstruction) continue @@ -147,6 +141,47 @@ object NextTurnAutomation { } } + private fun valueCityStateAlliance(civInfo: CivilizationInfo, cityState: CivilizationInfo): Int { + var value = 0 + if (!cityState.isAlive() || cityState.cities.isEmpty()) + return value + + if (civInfo.victoryType() == VictoryType.Cultural && cityState.canGiveStat(Stat.Culture)) { + value += 10 + } + else if (civInfo.victoryType() == VictoryType.Scientific && cityState.canGiveStat(Stat.Science)) { + // In case someone mods this in + value += 10 + } + else if (civInfo.victoryType() == VictoryType.Domination) { + // Don't ally close city-states, conquer them instead + val distance = getMinDistanceBetweenCities(civInfo, cityState) + if (distance < 20) + value -= (20 - distance) / 4 + } + if (civInfo.gold < 100) { + // Consider bullying for cash + value -= 5 + } + if (civInfo.getHappiness() < 5 && cityState.canGiveStat(Stat.Happiness)) { + value += 10 - civInfo.getHappiness() + } + if (civInfo.getHappiness() > 5 && cityState.canGiveStat(Stat.Food)) { + value += 5 + } + if (cityState.getAllyCiv() != null && cityState.getAllyCiv() != civInfo.civName) { + // easier not to compete if a third civ has this locked down + val thirdCivInfluence = cityState.getDiplomacyManager(cityState.getAllyCiv()!!).influence.toInt() + value -= (thirdCivInfluence - 60) / 10 + } + + // Bonus for luxury resources we can get from them + value += cityState.detailedCivResources.count { it.resource.resourceType == ResourceType.Luxury + && !(it.resource in civInfo.detailedCivResources) } + + return value + } + private fun protectCityStates(civInfo: CivilizationInfo) { for (state in civInfo.getKnownCivs().filter{!it.isDefeated() && it.isCityState()}) { val diplomacyManager = state.getDiplomacyManager(civInfo.civName) @@ -161,6 +196,21 @@ object NextTurnAutomation { } } + private fun bullyCityStates(civInfo: CivilizationInfo) { + for (state in civInfo.getKnownCivs().filter{!it.isDefeated() && it.isCityState()}) { + val diplomacyManager = state.getDiplomacyManager(civInfo.civName) + if(diplomacyManager.relationshipLevel() < RelationshipLevel.Friend + && diplomacyManager.diplomaticStatus == DiplomaticStatus.Peace + && valueCityStateAlliance(civInfo, state) <= 0 + && state.getTributeWillingness(civInfo) > 0) { + if (state.getTributeWillingness(civInfo, demandingWorker = true) > 0) + civInfo.demandWorker(state) + else + civInfo.demandGold(state) + } + } + } + private fun getFreeTechForCityStates(civInfo: CivilizationInfo) { // City-States automatically get all techs that at least half of the major civs know val researchableTechs = civInfo.gameInfo.ruleSet.technologies.keys diff --git a/core/src/com/unciv/logic/city/CityInfo.kt b/core/src/com/unciv/logic/city/CityInfo.kt index c2b6db6ff5..af9cb66d48 100644 --- a/core/src/com/unciv/logic/city/CityInfo.kt +++ b/core/src/com/unciv/logic/city/CityInfo.kt @@ -1,6 +1,7 @@ package com.unciv.logic.city import com.badlogic.gdx.math.Vector2 +import com.unciv.logic.battle.CityCombatant import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.GreatPersonManager import com.unciv.logic.civilization.ReligionState @@ -22,6 +23,7 @@ import kotlin.collections.HashMap import kotlin.collections.HashSet import kotlin.math.ceil import kotlin.math.min +import kotlin.math.pow import kotlin.math.roundToInt class CityInfo { @@ -673,6 +675,11 @@ class CityInfo { return !isOriginalCapital && !isHolyCity() && (!isCapital() || justCaptured) } + fun getForceEvaluation(): Int { + // Same as for units, so higher values count more + return CityCombatant(this).getCityStrength().toFloat().pow(1.5f).toInt() + } + fun getNeighbouringCivs(): List { val tilesList: HashSet = getTiles().toHashSet() @@ -688,7 +695,7 @@ class CityInfo { .distinct().toList() } fun getImprovableTiles(): Sequence = getTiles() - .filter {it.hasViewableResource(civInfo) && it.improvement == null} - + .filter {it.hasViewableResource(civInfo) && it.improvement == null} + //endregion -} +} \ No newline at end of file diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index a1cf74bdf7..bbc30bff24 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -7,11 +7,13 @@ import com.unciv.logic.GameInfo import com.unciv.logic.UncivShowableException import com.unciv.logic.automation.NextTurnAutomation import com.unciv.logic.automation.WorkerAutomation +import com.unciv.logic.battle.CityCombatant import com.unciv.logic.city.CityInfo import com.unciv.logic.civilization.RuinsManager.RuinsManager import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.civilization.diplomacy.DiplomacyManager import com.unciv.logic.civilization.diplomacy.DiplomaticStatus +import com.unciv.logic.civilization.diplomacy.RelationshipLevel import com.unciv.logic.map.MapUnit import com.unciv.logic.map.TileInfo import com.unciv.logic.map.UnitMovementAlgorithms @@ -26,14 +28,20 @@ import com.unciv.models.ruleset.tile.TileResource import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.stats.Stat import com.unciv.models.stats.Stats +import com.unciv.models.translations.getPlaceholderParameters +import com.unciv.models.translations.getPlaceholderText import com.unciv.models.translations.tr import com.unciv.ui.victoryscreen.RankingType import java.util.* import kotlin.collections.ArrayList import kotlin.collections.HashMap +import kotlin.collections.LinkedHashMap +import kotlin.math.max import kotlin.math.min import kotlin.math.pow import kotlin.math.roundToInt +import kotlin.system.measureNanoTime +import kotlin.system.measureTimeMillis class CivilizationInfo { @@ -977,6 +985,156 @@ class CivilizationInfo { } } + fun getTributeWillingness(demandingCiv: CivilizationInfo, demandingWorker: Boolean = false): Int { + return getTributeModifiers(demandingCiv, demandingWorker).values.sum() + } + + fun getTributeModifiers(demandingCiv: CivilizationInfo, demandingWorker: Boolean = false, requireWholeList: Boolean = false): HashMap { + val modifiers = LinkedHashMap() // Linked to preserve order when presenting the modifiers table + // Can't bully major civs or unsettled CS's + if (!isCityState()) { + modifiers["Major Civ"] = -999 + return modifiers + } + if (cities.isEmpty()) { + modifiers["No Cities"] = -999 + return modifiers + } + + modifiers["Base value"] = -110 + + if (cityStatePersonality == CityStatePersonality.Hostile) + modifiers["Hostile"] = -10 + if (cityStateType == CityStateType.Militaristic) + modifiers["Militaristic"] = -10 + if (allyCivName != null && allyCivName != demandingCiv.civName) + modifiers["Has Ally"] = -10 + if (getProtectorCivs().any { it != demandingCiv }) + modifiers["Has Protector"] = -20 + if (demandingWorker) + modifiers["Demanding a Worker"] = -30 + if (demandingWorker && getCapital().population.population < 4) + modifiers["Demanding a Worker from small City-State"] = -300 + val recentBullying = flagsCountdown[CivFlags.RecentlyBullied.name] + if (recentBullying != null && recentBullying > 10) + modifiers["Very recently paid tribute"] = -300 + else if (recentBullying != null) + modifiers["Recently paid tribute"] = -40 + if (getDiplomacyManager(demandingCiv).influence < -30) + modifiers["Influence below -30"] = -300 + + // Slight optimization, we don't do the expensive stuff if we have no chance of getting a positive result + if (!requireWholeList && modifiers.values.sum() <= -200) + return modifiers + + val forceRank = gameInfo.getAliveMajorCivs().sortedByDescending { it.getStatForRanking(RankingType.Force) }.indexOf(demandingCiv) + modifiers["Military Rank"] = 100 - ((100 / gameInfo.gameParameters.players.size) * forceRank) + + if (!requireWholeList && modifiers.values.sum() <= -100) + return modifiers + + val bullyRange = max(5, gameInfo.tileMap.tileMatrix.size / 10) // Longer range for larger maps + val inRangeTiles = getCapital().getCenterTile().getTilesInDistanceRange(1..bullyRange) + val forceNearCity = inRangeTiles + .sumBy { if (it.militaryUnit?.civInfo == demandingCiv) + it.militaryUnit!!.getForceEvaluation() + else 0 + } + val csForce = getCapital().getForceEvaluation() + inRangeTiles + .sumBy { if (it.militaryUnit?.civInfo == this) + it.militaryUnit!!.getForceEvaluation() + else 0 + } + val forceRatio = forceNearCity.toFloat() / csForce.toFloat() + + modifiers["Military near City-State"] = when { + forceRatio > 3f -> 100 + forceRatio > 2f -> 80 + forceRatio > 1.5f -> 60 + forceRatio > 1f -> 40 + forceRatio > 0.5f -> 20 + else -> 0 + } + + return modifiers + } + + fun goldGainedByTribute(): Int { + // These values are close enough, linear increase throughout the game + var gold = when (gameInfo.gameParameters.gameSpeed) { + GameSpeed.Quick -> 60 + GameSpeed.Standard -> 50 + GameSpeed.Epic -> 35 + GameSpeed.Marathon -> 30 + } + val turnsToIncrement = when (gameInfo.gameParameters.gameSpeed) { + GameSpeed.Quick -> 5f + GameSpeed.Standard -> 6.5f + GameSpeed.Epic -> 14f + GameSpeed.Marathon -> 32f + } + gold += 5 * (gameInfo.turns / turnsToIncrement).toInt() + + return gold + } + + fun demandGold(cityState: CivilizationInfo) { + if (!cityState.isCityState()) throw Exception("You can only demand gold from City-States!") + val goldAmount = goldGainedByTribute() + addGold(goldAmount) + cityState.getDiplomacyManager(this).influence -= 15 + cityState.addFlag(CivFlags.RecentlyBullied.name, 20) + cityState.updateAllyCivForCityState() + updateStatsForNextTurn() + } + + fun demandWorker(cityState: CivilizationInfo) { + if (!cityState.isCityState()) throw Exception("You can only demand workers from City-States!") + + val buildableWorkerLikeUnits = gameInfo.ruleSet.units.filter { + it.value.uniqueObjects.any { it.placeholderText == Constants.canBuildImprovements } + && it.value.isBuildable(this) + && it.value.isCivilian() + } + if (buildableWorkerLikeUnits.isEmpty()) return // Bad luck? + placeUnitNearTile(cityState.getCapital().location, buildableWorkerLikeUnits.keys.random()) + + cityState.getDiplomacyManager(this).influence -= 50 + cityState.addFlag(CivFlags.RecentlyBullied.name, 20) + cityState.updateAllyCivForCityState() + } + + fun canGiveStat(statType: Stat): Boolean { + if (!isCityState()) + return false + val eraInfo = getEraObject() + val bonuses = if (eraInfo == null) null + else eraInfo.allyBonus[cityStateType.name] + if (bonuses != null) { + // Defined city states in json + bonuses.addAll(eraInfo!!.friendBonus[cityStateType.name]!!) + for (bonus in bonuses) { + if (statType == Stat.Happiness && bonus.getPlaceholderText() == "Provides [] Happiness") + return true + if (bonus.getPlaceholderText() == "Provides [] [] per turn" && bonus.getPlaceholderParameters()[1] == statType.name) + return true + if (bonus.getPlaceholderText() == "Provides [] [] []" && bonus.getPlaceholderParameters()[1] == statType.name) + return true + } + + } else { + // compatibility mode + return when { + cityStateType == CityStateType.Mercantile && statType == Stat.Happiness -> true + cityStateType == CityStateType.Cultured && statType == Stat.Culture -> true + cityStateType == CityStateType.Maritime && statType == Stat.Food -> true + else -> false + } + } + + return false + } + //endregion } @@ -993,4 +1151,5 @@ enum class CivFlags { TurnsTillNextDiplomaticVote, ShowDiplomaticVotingResults, ShouldResetDiplomaticVotes, + RecentlyBullied } diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt index f5ffea50da..34d4d505b3 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt @@ -14,6 +14,7 @@ import kotlin.math.min enum class RelationshipLevel{ Unforgivable, + Afraid, Enemy, Competitor, Neutral, @@ -35,7 +36,8 @@ enum class DiplomacyFlags{ AgreedToNotSettleNearUs, IgnoreThemSettlingNearUs, ProvideMilitaryUnit, - EverBeenFriends + EverBeenFriends, + NotifiedAfraid } enum class DiplomaticModifiers{ @@ -142,6 +144,7 @@ class DiplomacyManager() { if (civInfo.isCityState()) { if (influence <= -30 || civInfo.isAtWarWith(otherCiv())) return RelationshipLevel.Unforgivable + if (influence < 30 && civInfo.getTributeWillingness(otherCiv()) > 0) return RelationshipLevel.Afraid if (influence < 0) return RelationshipLevel.Enemy if (influence >= 60 && civInfo.getAllyCiv() == otherCivName) return RelationshipLevel.Ally if (influence >= 30) return RelationshipLevel.Friend @@ -400,6 +403,16 @@ class DiplomacyManager() { if (civCapitalLocation != null) otherCiv().addNotification(text, civCapitalLocation, civInfo.civName, NotificationIcon.Diplomacy) else otherCiv().addNotification(text, civInfo.civName, NotificationIcon.Diplomacy) } + + // Potentially notify about afraid status + if (influence < 30 // We usually don't want to bully our friends + && !hasFlag(DiplomacyFlags.NotifiedAfraid) + && civInfo.getTributeWillingness(otherCiv()) > 0) { + setFlag(DiplomacyFlags.NotifiedAfraid, 20) // Wait 20 turns until next reminder + val text = "[${civInfo.civName}] is afraid of your military power!" + if (civCapitalLocation != null) otherCiv().addNotification(text, civCapitalLocation, civInfo.civName, NotificationIcon.Diplomacy) + else otherCiv().addNotification(text, civInfo.civName, NotificationIcon.Diplomacy) + } } } diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index bfd30d5e12..1b61f0c21e 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -16,6 +16,7 @@ import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.ruleset.unit.UnitType import com.unciv.ui.utils.toPercent import java.text.DecimalFormat +import kotlin.math.pow /** * The immutable properties and mutable game state of an individual unit present on the map @@ -985,5 +986,13 @@ class MapUnit { if (isPreparingParadrop()) action = null } + fun getForceEvaluation(): Int { + val promotionBonus = (promotions.numberOfPromotions + 1).toFloat().pow(0.3f) + var power = (baseUnit.getForceEvaluation() * promotionBonus).toInt() + power *= health + power /= 100 + return power + } + //endregion } diff --git a/core/src/com/unciv/logic/trade/TradeEvaluation.kt b/core/src/com/unciv/logic/trade/TradeEvaluation.kt index c7363af8a7..2aaea11de8 100644 --- a/core/src/com/unciv/logic/trade/TradeEvaluation.kt +++ b/core/src/com/unciv/logic/trade/TradeEvaluation.kt @@ -243,7 +243,7 @@ class TradeEvaluation { RelationshipLevel.Unforgivable -> 10000 RelationshipLevel.Enemy -> 2000 RelationshipLevel.Competitor -> 500 - RelationshipLevel.Neutral -> 200 + RelationshipLevel.Neutral, RelationshipLevel.Afraid -> 200 RelationshipLevel.Favorable, RelationshipLevel.Friend, RelationshipLevel.Ally -> 100 } } diff --git a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt index 057f4a8971..747bb37062 100644 --- a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt +++ b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt @@ -19,6 +19,7 @@ import java.util.* import kotlin.collections.ArrayList import kotlin.collections.HashMap import kotlin.collections.HashSet +import kotlin.math.pow // This is BaseUnit because Unit is already a base Kotlin class and to avoid mixing the two up @@ -55,6 +56,9 @@ class BaseUnit : INamed, INonPerpetualConstruction, ICivilopediaText { var uniqueTo: String? = null var attackSound: String? = null + @Transient + var cachedForceEvaluation: Int = -1 + lateinit var ruleset: Ruleset override var civilopediaText = listOf() @@ -444,4 +448,78 @@ class BaseUnit : INamed, INonPerpetualConstruction, ICivilopediaText { && (uniqueObjects + getType().uniqueObjects) .any { it.placeholderText == "+[]% Strength vs []" && it.params[1] == "City" } ) + + fun getForceEvaluation(): Int { + if (cachedForceEvaluation < 0) evaluateForce() + return cachedForceEvaluation + } + + private fun evaluateForce() { + if (strength == 0 && rangedStrength == 0) { + cachedForceEvaluation = 0 + return + } + + var power = strength.toFloat().pow(1.5f).toInt() + var rangedPower = rangedStrength.toFloat().pow(1.45f).toInt() + + // Value ranged naval units less + if (isWaterUnit()) { + rangedPower /= 2 + } + if (rangedPower > 0) + power = rangedPower + + // Replicates the formula from civ V, which is a lower multiplier than probably intended, because math + // They did fix it in BNW so it was completely bugged and always 1, again math + power = (power * movement.toFloat().pow(0.3f)).toInt() + + if (uniqueObjects.any { it.placeholderText =="Self-destructs when attacking" } ) + power /= 2 + if (uniqueObjects.any { it.placeholderText =="Nuclear weapon of Strength []" } ) + power += 4000 + + // Uniques + for (unique in uniqueObjects) { + + when { + unique.placeholderText == "+[]% Strength vs []" && unique.params[1] == "City" // City Attack - half the bonus + -> power += (power * unique.params[0].toInt()) / 200 + unique.placeholderText == "+[]% Strength vs []" && unique.params[1] != "City" // Bonus vs something else - a quarter of the bonus + -> power += (power * unique.params[0].toInt()) / 400 + unique.placeholderText == "+[]% Strength when attacking" // Attack - half the bonus + -> power += (power * unique.params[0].toInt()) / 200 + unique.placeholderText == "+[]% Strength when defending" // Defense - half the bonus + -> power += (power * unique.params[0].toInt()) / 200 + unique.placeholderText == "May Paradrop up to [] tiles from inside friendly territory" // Paradrop - 25% bonus + -> power += power / 4 + unique.placeholderText == "Must set up to ranged attack" // Must set up - 20 % penalty + -> power -= power / 5 + unique.placeholderText == "+[]% Strength in []" // Bonus in terrain or feature - half the bonus + -> power += (power * unique.params[0].toInt()) / 200 + } + } + + // Base promotions + for (promotionName in promotions) { + for (unique in ruleset.unitPromotions[promotionName]!!.uniqueObjects) { + when { + unique.placeholderText == "+[]% Strength vs []" && unique.params[1] == "City" // City Attack - half the bonus + -> power += (power * unique.params[0].toInt()) / 200 + unique.placeholderText == "+[]% Strength vs []" && unique.params[1] != "City" // Bonus vs something else - a quarter of the bonus + -> power += (power * unique.params[0].toInt()) / 400 + unique.placeholderText == "+[]% Strength when attacking" // Attack - half the bonus + -> power += (power * unique.params[0].toInt()) / 200 + unique.placeholderText == "+[]% Strength when defending" // Defense - half the bonus + -> power += (power * unique.params[0].toInt()) / 200 + unique.placeholderText == "[] additional attacks per turn" // Extra attacks - 20% bonus per extra attack + -> power += (power * unique.params[0].toInt()) / 5 + unique.placeholderText == "+[]% Strength in []" // Bonus in terrain or feature - half the bonus + -> power += (power * unique.params[0].toInt()) / 200 + } + } + + } + cachedForceEvaluation = power + } } diff --git a/core/src/com/unciv/ui/tilegroups/CityButton.kt b/core/src/com/unciv/ui/tilegroups/CityButton.kt index 66fb569eda..13cf71245b 100644 --- a/core/src/com/unciv/ui/tilegroups/CityButton.kt +++ b/core/src/com/unciv/ui/tilegroups/CityButton.kt @@ -409,6 +409,7 @@ class CityButton(val city: CityInfo, private val tileGroup: WorldTileGroup): Tab val color = when (relationshipLevel) { RelationshipLevel.Unforgivable -> Color.RED RelationshipLevel.Enemy -> Color.ORANGE + RelationshipLevel.Afraid -> Color.YELLOW RelationshipLevel.Neutral, RelationshipLevel.Friend -> Color.LIME RelationshipLevel.Ally -> Color.SKY else -> Color.DARK_GRAY diff --git a/core/src/com/unciv/ui/trade/DiplomacyScreen.kt b/core/src/com/unciv/ui/trade/DiplomacyScreen.kt index c58ca69834..effb3590cc 100644 --- a/core/src/com/unciv/ui/trade/DiplomacyScreen.kt +++ b/core/src/com/unciv/ui/trade/DiplomacyScreen.kt @@ -239,6 +239,14 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() { if (isNotPlayersTurn()) protectionButton.disable() } + val demandTributeButton = "Demand Tribute".toTextButton() + demandTributeButton.onClick { + rightSideTable.clear() + rightSideTable.add(ScrollPane(getDemandTributeTable(otherCiv))) + } + diplomacyTable.add(demandTributeButton).row() + if (isNotPlayersTurn()) demandTributeButton.disable() + val diplomacyManager = viewingCiv.getDiplomacyManager(otherCiv) if (!viewingCiv.gameInfo.ruleSet.modOptions.uniques.contains(ModOptionsConstants.diplomaticRelationshipsCannotChange)) { if (viewingCiv.isAtWarWith(otherCiv)) { @@ -384,6 +392,50 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() { } + private fun getDemandTributeTable(otherCiv: CivilizationInfo): Table { + val diplomacyTable = getCityStateDiplomacyTableHeader(otherCiv) + diplomacyTable.addSeparator() + diplomacyTable.add("Tribute Willingness".toLabel()).row() + diplomacyTable.add(">0 to take gold, >30 and size 4 city for worker".toLabel()).row() + val modifierTable = Table() + val tributeModifiers = otherCiv.getTributeModifiers(viewingCiv, requireWholeList = true) + for (item in tributeModifiers) { + val color = if (item.value > 0) Color.GREEN else Color.RED + modifierTable.add(item.key.toLabel(color)) + modifierTable.add(item.value.toString().toLabel(color)).row() + } + modifierTable.add("Sum:".toLabel()) + modifierTable.add(tributeModifiers.values.sum().toLabel()).row() + diplomacyTable.add(modifierTable).row() + diplomacyTable.addSeparator() + + val demandGoldButton = "Take [${otherCiv.goldGainedByTribute()}] gold (-15 Influence)".toTextButton() + demandGoldButton.onClick { + viewingCiv.demandGold(otherCiv) + rightSideTable.clear() + rightSideTable.add(ScrollPane(getCityStateDiplomacyTable(otherCiv))) + } + diplomacyTable.add(demandGoldButton).row() + if (otherCiv.getTributeWillingness(viewingCiv, demandingWorker = false) <= 0) demandGoldButton.disable() + + val demandWorkerButton = "Take worker (-50 Influence)".toTextButton() + demandWorkerButton.onClick { + viewingCiv.demandWorker(otherCiv) + rightSideTable.clear() + rightSideTable.add(ScrollPane(getCityStateDiplomacyTable(otherCiv))) + } + diplomacyTable.add(demandWorkerButton).row() + if (otherCiv.getTributeWillingness(viewingCiv, demandingWorker = true) <= 0) demandWorkerButton.disable() + + val backButton = "Back".toTextButton() + backButton.onClick { + rightSideTable.clear() + rightSideTable.add(ScrollPane(getCityStateDiplomacyTable(otherCiv))) + } + diplomacyTable.add(backButton) + return diplomacyTable + } + private fun getQuestTable(assignedQuest: AssignedQuest): Table { val questTable = Table() questTable.defaults().pad(10f) @@ -630,6 +682,7 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() { RelationshipLevel.Neutral -> Color.WHITE RelationshipLevel.Favorable, RelationshipLevel.Friend, RelationshipLevel.Ally -> Color.GREEN + RelationshipLevel.Afraid -> Color.YELLOW else -> Color.RED }