Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions android/assets/jsons/translations/Russian.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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 = Включенные количества ресурсов
Expand Down Expand Up @@ -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 = Следует использовать только в качестве постоянного аудиовизуального мода
Expand Down
2 changes: 2 additions & 0 deletions android/assets/jsons/translations/template.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
10 changes: 7 additions & 3 deletions core/src/com/unciv/logic/battle/Battle.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions core/src/com/unciv/logic/civilization/Civilization.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
21 changes: 21 additions & 0 deletions core/src/com/unciv/logic/trade/TradeEvaluation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 }
}
30 changes: 23 additions & 7 deletions core/src/com/unciv/logic/trade/TradeLogic.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions core/src/com/unciv/models/Spy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions core/src/com/unciv/models/ruleset/unique/UniqueType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 <for [AI player] Civilizations>. 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 <for [AI player] Civilizations>."),
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),

Expand Down
8 changes: 6 additions & 2 deletions core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down Expand Up @@ -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()
}
Expand Down
Loading