diff --git a/android/assets/jsons/translations/Russian.properties b/android/assets/jsons/translations/Russian.properties index 46e9db8a4ba48..e3a7bbe180633 100644 --- a/android/assets/jsons/translations/Russian.properties +++ b/android/assets/jsons/translations/Russian.properties @@ -461,6 +461,8 @@ Height = Высота Width = Ширина Radius = Радиус Enable Espionage = Разрешить шпионаж +No settlers for AI civilizations = Нет поселенцев для цивилизаций ИИ +No world wonders for AI civilizations = Нет чудес света для цивилизаций ИИ Resource Setting = Ресурсы Enabled Resource Settings = Включенные количества ресурсов @@ -2688,6 +2690,14 @@ Disable religion = Отключить религию Can only start games from the starting era = Может начинать игры только со стартовой эпохи Allow raze capital = Разрешить разрушать столицу Allow raze holy city = Разрешить разрушать святые города +Always accepts white peace = Всегда принимает белый мир +Cities cannot be liberated = Города нельзя освобождать +Cannot vote in World Congress = Нельзя голосовать во Всемирном конгрессе +Cannot trade with [civFilter] Civilizations = Нельзя торговать с цивилизациями [civFilter] +May not found a religion = Нельзя основать религию +Cannot spread religion to [civFilter] Civilizations = Нельзя распространять религию цивилизациям [civFilter] +Cannot coup [civFilter] Civilizations = Нельзя устраивать перевороты у цивилизаций [civFilter] +No XP from fighting [civFilter] Civilizations = Нет опыта за бой с цивилизациями [civFilter] Mod is incompatible with [modFilter] = Мод несовместим с [modFilter] Mod requires [modFilter] = Мод требует [modFilter] Should only be used as permanent audiovisual mod = Следует использовать только в качестве постоянного аудиовизуального мода diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 9b912f760265c..f67429e66542f 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -462,6 +462,8 @@ Height = Width = Radius = Enable Espionage = +No settlers for AI civilizations = +No world wonders for AI civilizations = Resource Setting = Enabled Resource Settings = diff --git a/core/src/com/unciv/logic/automation/civilization/ReligionAutomation.kt b/core/src/com/unciv/logic/automation/civilization/ReligionAutomation.kt index 306c5115ccc23..58ca4d237d817 100644 --- a/core/src/com/unciv/logic/automation/civilization/ReligionAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/ReligionAutomation.kt @@ -437,6 +437,7 @@ object ReligionAutomation { } private fun foundReligion(civInfo: Civilization) { + if (civInfo.hasUnique(UniqueType.MayNotFoundReligion)) return if (civInfo.religionManager.religionState != ReligionState.FoundingReligion) return val rng = civInfo.state.stateBasedRandom("ReligionAutomation.foundReligion") val usedReligions = civInfo.gameInfo.religions.values.mapTo(mutableSetOf()) { it.name } diff --git a/core/src/com/unciv/logic/automation/unit/ReligiousUnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/ReligiousUnitAutomation.kt index 178e24bc8ea38..de6223fddfceb 100644 --- a/core/src/com/unciv/logic/automation/unit/ReligiousUnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/ReligiousUnitAutomation.kt @@ -24,6 +24,8 @@ object ReligiousUnitAutomation { @Readonly fun isValidSpreadReligionTarget(city: City): Boolean { + if (unit.civ.getMatchingUniques(UniqueType.CannotSpreadReligionTo).any { city.civ.matchesFilter(it.params[0]) }) + return false val diplomacyManager = unit.civ.getDiplomacyManager(city.civ) if (diplomacyManager?.hasFlag(DiplomacyFlags.AgreedToNotSpreadReligion) == true){ // See NextTurnAutomation - these are the conditions under which AI agrees to religious demands @@ -158,6 +160,7 @@ object ReligiousUnitAutomation { fun foundReligion(unit: MapUnit) { + if (unit.civ.hasUnique(UniqueType.MayNotFoundReligion)) return val cityToFoundReligionAt = if (unit.getTile().isCityCenter() && !unit.getTile().owningCity!!.isHolyCity()) unit.getTile().owningCity else unit.civ.cities.firstOrNull { diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index 448f45a887fa5..8dfb14602c4fc 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -599,15 +599,19 @@ object Battle { internal fun addXp(thisCombatant: ICombatant, amount: Int, otherCombatant: ICombatant) { if (thisCombatant !is MapUnitCombatant) return val civ = thisCombatant.getCivInfo() - val otherIsBarbarian = otherCombatant.getCivInfo().isBarbarian + val otherCiv = otherCombatant.getCivInfo() + val otherIsBarbarian = otherCiv.isBarbarian val promotions = thisCombatant.unit.promotions val modConstants = civ.gameInfo.ruleset.modOptions.constants if (otherIsBarbarian && promotions.totalXpProduced() >= modConstants.maxXPfromBarbarians) return - val unitCouldAlreadyPromote = promotions.canBePromoted() - val gameContext = GameContext(civInfo = civ, ourCombatant = thisCombatant, theirCombatant = otherCombatant) + if (civ.getMatchingUniques(UniqueType.NoXpFromFighting, gameContext).any { + otherCiv.matchesFilter(it.params[0]) + }) + return + val unitCouldAlreadyPromote = promotions.canBePromoted() val baseXP = amount + thisCombatant .getMatchingUniques(UniqueType.FlatXPGain, gameContext, true) diff --git a/core/src/com/unciv/logic/city/managers/CityConquestFunctions.kt b/core/src/com/unciv/logic/city/managers/CityConquestFunctions.kt index f9a5bf0b17323..ac25af615c1fd 100644 --- a/core/src/com/unciv/logic/city/managers/CityConquestFunctions.kt +++ b/core/src/com/unciv/logic/city/managers/CityConquestFunctions.kt @@ -208,6 +208,10 @@ class CityConquestFunctions(val city: City) { } val foundingCiv = city.foundingCivObject!! + if (foundingCiv.isMajorCiv() && foundingCiv.hasUnique(UniqueType.CitiesCannotBeLiberated)) { + this.puppetCity(conqueringCiv) + return + } val oldCiv = city.civ diplomaticRepercussionsForLiberatingCity(conqueringCiv, oldCiv) diff --git a/core/src/com/unciv/logic/civilization/Civilization.kt b/core/src/com/unciv/logic/civilization/Civilization.kt index 69c4861e6acab..2403d4481491b 100644 --- a/core/src/com/unciv/logic/civilization/Civilization.kt +++ b/core/src/com/unciv/logic/civilization/Civilization.kt @@ -987,6 +987,7 @@ class Civilization : IsPartOfGameInfoSerialization { // Does not need checks for Barbarians or dead civs because the callers already ensure that // (NextTurnAutomation.tryVoteForDiplomaticVictory and NextTurnAction.WorldCongressVote) !isSpectator() + && !hasUnique(UniqueType.CannotVoteInWorldCongress) && getTurnsTillNextDiplomaticVote() == 0 && civID !in gameInfo.diplomaticVictoryVotesCast.keys // Only vote if there is someone to vote for, may happen in one-more-turn mode diff --git a/core/src/com/unciv/logic/civilization/managers/ReligionManager.kt b/core/src/com/unciv/logic/civilization/managers/ReligionManager.kt index b50bb81891b67..3cc8cfa993db9 100644 --- a/core/src/com/unciv/logic/civilization/managers/ReligionManager.kt +++ b/core/src/com/unciv/logic/civilization/managers/ReligionManager.kt @@ -299,6 +299,7 @@ class ReligionManager : IsPartOfGameInfoSerialization { if (religionState >= ReligionState.Religion) return false // Already created a major religion if (!civInfo.isMajorCiv()) return false // Only major civs may use religion + if (civInfo.hasUnique(UniqueType.MayNotFoundReligion)) return false if (remainingFoundableReligions() == 0) return false // Too bad, too many religions have already been founded @@ -521,6 +522,9 @@ class ReligionManager : IsPartOfGameInfoSerialization { if (missionary.currentTile.owningCity?.religion?.getMajorityReligion()?.name == missionary.religion) return false if (missionary.getTile().getCity()!!.religion.isProtectedByInquisitor(missionary.religion)) return false + val owner = missionary.getTile().getOwner()!! + if (civInfo.getMatchingUniques(UniqueType.CannotSpreadReligionTo).any { owner.matchesFilter(it.params[0]) }) + return false return true } diff --git a/core/src/com/unciv/logic/trade/TradeEvaluation.kt b/core/src/com/unciv/logic/trade/TradeEvaluation.kt index 08dd2803bb545..8a6abf8399fcb 100644 --- a/core/src/com/unciv/logic/trade/TradeEvaluation.kt +++ b/core/src/com/unciv/logic/trade/TradeEvaluation.kt @@ -33,6 +33,11 @@ class TradeEvaluation { || (tradePartner.hasEverOwnedOriginalCapital && trade.theirOffers.count { it.type == TradeOfferType.City } == tradePartner.cities.size)) { return false } + + if ((offerer.getMatchingUniques(UniqueType.CannotTradeWith).any { tradePartner.matchesFilter(it.params[0]) } + || tradePartner.getMatchingUniques(UniqueType.CannotTradeWith).any { offerer.matchesFilter(it.params[0]) }) + && !trade.isPeaceTreaty()) + return false // No way to tell who offers what in isOfferValid() val embassyOffer = TradeOffer(Constants.acceptEmbassy, TradeOfferType.Embassy, speed = offerer.gameInfo.speed) @@ -101,11 +106,18 @@ class TradeEvaluation { @Readonly fun isTradeAcceptable(trade: Trade, evaluator: Civilization, tradePartner: Civilization): Boolean { + if (evaluator.hasUnique(UniqueType.AlwaysAcceptsWhitePeace) + && isWhitePeace(trade)) + return true return getTradeAcceptability(trade, evaluator, tradePartner, true) >= 0 } @Readonly fun getTradeAcceptability(trade: Trade, evaluator: Civilization, tradePartner: Civilization, includeDiplomaticGifts:Boolean = false): Int { + if (evaluator.hasUnique(UniqueType.AlwaysAcceptsWhitePeace) + && isWhitePeace(trade)) + return 1 + val citiesAskedToSurrender = trade.ourOffers.count { it.type == TradeOfferType.City } val maxCitiesToSurrender = ceil(evaluator.cities.size.toFloat() / 5).toInt() if (citiesAskedToSurrender > maxCitiesToSurrender) { @@ -509,6 +521,8 @@ class TradeEvaluation { @Readonly fun evaluatePeaceCostForThem(ourCiv: Civilization, otherCiv: Civilization): Int { + if (ourCiv.hasUnique(UniqueType.AlwaysAcceptsWhitePeace)) + return 0 val ourCombatStrength = ourCiv.getStatForRanking(RankingType.Force) val theirCombatStrength = otherCiv.getStatForRanking(RankingType.Force) if (ourCombatStrength * 1.5f >= theirCombatStrength && theirCombatStrength * 1.5f >= ourCombatStrength) @@ -558,4 +572,11 @@ class TradeEvaluation { ?: return 0 return unique.params[0].toInt() } + + /** Peace treaty with no other offers on either side. */ + @Readonly + private fun isWhitePeace(trade: Trade) = + trade.isPeaceTreaty() + && trade.ourOffers.all { it.name == Constants.peaceTreaty } + && trade.theirOffers.all { it.name == Constants.peaceTreaty } } diff --git a/core/src/com/unciv/logic/trade/TradeLogic.kt b/core/src/com/unciv/logic/trade/TradeLogic.kt index 7ca7ae8672c54..b9303d47695b0 100644 --- a/core/src/com/unciv/logic/trade/TradeLogic.kt +++ b/core/src/com/unciv/logic/trade/TradeLogic.kt @@ -22,13 +22,21 @@ class TradeLogic(val ourCivilization: Civilization, val otherCivilization: Civil val currentTrade = Trade() init { - // Embassy trade availability depends solely on our ability to trade it - val embassyOffer = TradeOffer(Constants.acceptEmbassy, TradeOfferType.Embassy, speed = ourCivilization.gameInfo.speed) - if (ourCivilization.diplomacyFunctions.canEstablishEmbassyWith(otherCivilization)) - theirAvailableOffers.add(embassyOffer) + val onlyPeaceTrade = ourCivilization.getMatchingUniques(UniqueType.CannotTradeWith).any { + otherCivilization.matchesFilter(it.params[0]) + } || otherCivilization.getMatchingUniques(UniqueType.CannotTradeWith).any { + ourCivilization.matchesFilter(it.params[0]) + } + + if (!onlyPeaceTrade) { + // Embassy trade availability depends solely on our ability to trade it + val embassyOffer = TradeOffer(Constants.acceptEmbassy, TradeOfferType.Embassy, speed = ourCivilization.gameInfo.speed) + if (ourCivilization.diplomacyFunctions.canEstablishEmbassyWith(otherCivilization)) + theirAvailableOffers.add(embassyOffer) - if (ourCivilization.diplomacyFunctions.canOfferEmbassyTo(otherCivilization)) - ourAvailableOffers.add(embassyOffer) + if (ourCivilization.diplomacyFunctions.canOfferEmbassyTo(otherCivilization)) + ourAvailableOffers.add(embassyOffer) + } // Other trade items are added as usual for both sides ourAvailableOffers += getAvailableOffers(ourCivilization, otherCivilization) @@ -39,10 +47,18 @@ class TradeLogic(val ourCivilization: Civilization, val otherCivilization: Civil private fun getAvailableOffers(civInfo: Civilization, otherCiv: Civilization): TradeOffersList { val offers = TradeOffersList() if (civInfo.isCityState || otherCiv.isCityState) return offers + + val onlyPeaceTrade = civInfo.getMatchingUniques(UniqueType.CannotTradeWith).any { + otherCiv.matchesFilter(it.params[0]) + } || otherCiv.getMatchingUniques(UniqueType.CannotTradeWith).any { + civInfo.matchesFilter(it.params[0]) + } if (civInfo.isAtWarWith(otherCiv)) offers.add(TradeOffer(Constants.peaceTreaty, TradeOfferType.Treaty, speed = civInfo.gameInfo.speed)) - + + if (onlyPeaceTrade) return offers + if (civInfo.diplomacyFunctions.meetsEmbassyRequirementFor(otherCiv) && !otherCiv.getDiplomacyManager(civInfo)!!.hasOpenBorders && civInfo.hasUnique(UniqueType.EnablesOpenBorders) diff --git a/core/src/com/unciv/models/Spy.kt b/core/src/com/unciv/models/Spy.kt index 8391f6da63077..973e5253441c9 100644 --- a/core/src/com/unciv/models/Spy.kt +++ b/core/src/com/unciv/models/Spy.kt @@ -263,6 +263,7 @@ class Spy private constructor() : IsPartOfGameInfoSerialization { @Readonly fun canDoCoup(): Boolean = getCityOrNull() != null && getCity().civ.isCityState && isSetUp() && getCity().civ.allyCiv != civInfo + && civInfo.getMatchingUniques(UniqueType.CannotCoup).none { getCity().civ.matchesFilter(it.params[0]) } /** * Initiates a coup if this spies civ is not the ally of the city-state. diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt b/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt index 33f079c60185a..e70ba0e864246 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt @@ -181,7 +181,7 @@ object UniqueTriggerActivation { val unitName = unique.params[0] val baseUnit = ruleset.units[unitName] ?: return null val civUnit = civInfo.getEquivalentUnit(baseUnit) - if (civUnit.isCityFounder() && civInfo.isOneCityChallenger()) + if (civUnit.isCityFounder() && (civInfo.isOneCityChallenger() || !civUnit.isBuildable(civInfo))) return null val limit = civUnit.getMatchingUniques(UniqueType.MaxNumberBuildable) @@ -222,7 +222,7 @@ object UniqueTriggerActivation { val unitName = unique.params[1] val baseUnit = ruleset.units[unitName] ?: return null val civUnit = civInfo.getEquivalentUnit(baseUnit) - if (civUnit.isCityFounder() && civInfo.isOneCityChallenger()) + if (civUnit.isCityFounder() && (civInfo.isOneCityChallenger() || !civUnit.isBuildable(civInfo))) return null val limit = civUnit.getMatchingUniques(UniqueType.MaxNumberBuildable) @@ -279,7 +279,7 @@ object UniqueTriggerActivation { val unitName = unique.params[0] val baseUnit = ruleset.units[unitName] ?: return null val civUnit = civInfo.getEquivalentUnit(baseUnit) - if (civUnit.isCityFounder() && civInfo.isOneCityChallenger()) + if (civUnit.isCityFounder() && (civInfo.isOneCityChallenger() || !civUnit.isBuildable(civInfo))) return null fun placeUnit(): Boolean { val placedUnit = when { @@ -312,7 +312,7 @@ object UniqueTriggerActivation { val unitName = unique.params[1] val baseUnit = ruleset.units[unitName] ?: return null val civUnit = civInfo.getEquivalentUnit(baseUnit) - if (civUnit.isCityFounder() && civInfo.isOneCityChallenger()) + if (civUnit.isCityFounder() && (civInfo.isOneCityChallenger() || !civUnit.isBuildable(civInfo))) return null val limit = civUnit.getMatchingUniques(UniqueType.MaxNumberBuildable) .map { it.params[0].toInt() }.minOrNull() @@ -363,7 +363,7 @@ object UniqueTriggerActivation { val unitName = unique.params[0] val baseUnit = ruleset.units[unitName] ?: return null var civUnit = civInfo.getEquivalentUnit(baseUnit) - if (civUnit.isCityFounder() && civInfo.isOneCityChallenger()) { + if (civUnit.isCityFounder() && (civInfo.isOneCityChallenger() || !civUnit.isBuildable(civInfo))) { val replacementUnit = ruleset.units.values .firstOrNull { it.getMatchingUniques(UniqueType.BuildImprovements, GameContext.IgnoreConditionals) diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index 65cf078aacdde..09ad689a963a2 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -277,6 +277,8 @@ enum class UniqueType( docDescription = MULTIPLICATIVE_BONUS_EXPLANATION), ReligionSpreadDistance("Religion naturally spreads to cities [amount] tiles away", UniqueTarget.Global, UniqueTarget.FollowerBelief), MayNotGenerateGreatProphet("May not generate great prophet equivalents naturally", UniqueTarget.Global), + MayNotFoundReligion("May not found a religion", UniqueTarget.Global), + CannotSpreadReligionTo("Cannot spread religion to [civFilter] Civilizations", UniqueTarget.Global), FaithCostOfGreatProphetChange("[relativeAmount]% Faith cost of generating Great Prophet equivalents", UniqueTarget.Global), /// Espionage @@ -299,6 +301,14 @@ enum class UniqueType( /// Misc. MayBuyConstructionsInPuppets("May buy items in puppet cities", UniqueTarget.Global), MayNotAnnexCities("May not annex cities", UniqueTarget.Global), + CitiesCannotBeLiberated("Cities cannot be liberated", UniqueTarget.Global, + docDescription = "Typically used with . City-States can still be liberated unless they match the conditional."), + AlwaysAcceptsWhitePeace("Always accepts white peace", UniqueTarget.Global), + CannotVoteInWorldCongress("Cannot vote in World Congress", UniqueTarget.Global), + CannotTradeWith("Cannot trade with [civFilter] Civilizations", UniqueTarget.Global, + docDescription = "Peace treaties are still allowed. Typically used with ."), + CannotCoup("Cannot coup [civFilter] Civilizations", UniqueTarget.Global), + NoXpFromFighting("No XP from fighting [civFilter] Civilizations", UniqueTarget.Global), BorrowsCityNames("\"Borrows\" city names from other civilizations in the game", UniqueTarget.Global), CitiesAreRazedXTimesFaster("Cities are razed [amount] times as fast", UniqueTarget.Global), diff --git a/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt b/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt index 2b8b54418a286..8a3342481a59c 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt @@ -205,7 +205,9 @@ class AlertPopup( if (city.foundingCivObject != null && city.civ != city.foundingCivObject // can't liberate if the city actually belongs to those guys - && conqueringCiv != city.foundingCivObject) { // or belongs originally to us + && conqueringCiv != city.foundingCivObject // or belongs originally to us + && !(city.foundingCivObject!!.isMajorCiv() + && city.foundingCivObject!!.hasUnique(UniqueType.CitiesCannotBeLiberated))) { addLiberateOption(city, conqueringCiv) addSeparator() } @@ -245,7 +247,9 @@ class AlertPopup( addQuestionAboutTheCity(city.name) val conqueringCiv = gameInfo.getCurrentPlayerCivilization() - if (!conqueringCiv.isAtWarWith(city.foundingCivObject!!)) { + if (!conqueringCiv.isAtWarWith(city.foundingCivObject!!) + && !(city.foundingCivObject!!.isMajorCiv() + && city.foundingCivObject!!.hasUnique(UniqueType.CitiesCannotBeLiberated))) { addLiberateOption(city, conqueringCiv) addSeparator() }