diff --git a/android/assets/jsons/Civ V - Gods & Kings/Policies.json b/android/assets/jsons/Civ V - Gods & Kings/Policies.json index c9ce541f58..3aea98f5af 100644 --- a/android/assets/jsons/Civ V - Gods & Kings/Policies.json +++ b/android/assets/jsons/Civ V - Gods & Kings/Policies.json @@ -106,7 +106,7 @@ }, { "name": "Military Tradition", - "uniques":["[Military] units gain [50]% more Experience from combat"], + "uniques":["[+50]% XP gained from combat "], "requires": ["Warrior Code"], "row": 2, "column": 2 diff --git a/android/assets/jsons/Civ V - Gods & Kings/UnitPromotions.json b/android/assets/jsons/Civ V - Gods & Kings/UnitPromotions.json index 2df2e5d1e7..f1bab8b324 100644 --- a/android/assets/jsons/Civ V - Gods & Kings/UnitPromotions.json +++ b/android/assets/jsons/Civ V - Gods & Kings/UnitPromotions.json @@ -517,7 +517,7 @@ }, { "name": "Quick Study", // only for Keshik and subsequent upgrades - "uniques": ["[50]% Bonus XP gain"] + "uniques": ["[+50]% XP gained from combat"] }, { "name": "Haka War Dance", // only for Maori Warrior and subsequent upgrades diff --git a/android/assets/jsons/Civ V - Vanilla/Policies.json b/android/assets/jsons/Civ V - Vanilla/Policies.json index 0d9c07481e..d4ec6b71f6 100644 --- a/android/assets/jsons/Civ V - Vanilla/Policies.json +++ b/android/assets/jsons/Civ V - Vanilla/Policies.json @@ -107,7 +107,7 @@ }, { "name": "Military Tradition", - "uniques":["[Military] units gain [50]% more Experience from combat"], + "uniques":["[+50]% XP gained from combat "], "requires": ["Warrior Code"], "row": 2, "column": 2 diff --git a/android/assets/jsons/Civ V - Vanilla/UnitPromotions.json b/android/assets/jsons/Civ V - Vanilla/UnitPromotions.json index 2df2e5d1e7..f1bab8b324 100644 --- a/android/assets/jsons/Civ V - Vanilla/UnitPromotions.json +++ b/android/assets/jsons/Civ V - Vanilla/UnitPromotions.json @@ -517,7 +517,7 @@ }, { "name": "Quick Study", // only for Keshik and subsequent upgrades - "uniques": ["[50]% Bonus XP gain"] + "uniques": ["[+50]% XP gained from combat"] }, { "name": "Haka War Dance", // only for Maori Warrior and subsequent upgrades diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index cca05a1a84..39db899ee1 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -10,6 +10,7 @@ import com.unciv.logic.map.RoadStatus import com.unciv.logic.map.TileInfo import com.unciv.models.AttackableTile import com.unciv.models.UnitActionType +import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.stats.Stat @@ -39,7 +40,7 @@ object Battle { */ if (attacker.unit.currentMovement == 0f) return - if (attacker.unit.hasUnique(UniqueType.MustSetUp) && !attacker.unit.isSetUpForSiege()) { + if (attacker.hasUnique(UniqueType.MustSetUp) && !attacker.unit.isSetUpForSiege()) { attacker.unit.action = UnitActionType.SetUp.value attacker.unit.useMovementPoints(1f) } @@ -131,25 +132,22 @@ object Battle { private fun tryEarnFromKilling(civUnit: ICombatant, defeatedUnit: MapUnitCombatant) { val unitStr = max(defeatedUnit.unit.baseUnit.strength, defeatedUnit.unit.baseUnit.rangedStrength) val unitCost = defeatedUnit.unit.baseUnit.cost - var bonusUniquePlaceholderText = "Earn []% of killed [] unit's [] as []" val bonusUniques = ArrayList() - + val stateForConditionals = StateForConditionals(civInfo = civUnit.getCivInfo(), ourCombatant = civUnit, theirCombatant = defeatedUnit) if (civUnit is MapUnitCombatant) { - bonusUniques.addAll(civUnit.getMatchingUniques(bonusUniquePlaceholderText)) - bonusUniques.addAll(civUnit.getCivInfo().getMatchingUniques(bonusUniquePlaceholderText)) + bonusUniques.addAll(civUnit.getMatchingUniques(UniqueType.KillUnitPlunder, stateForConditionals, true)) } else { - bonusUniques.addAll(civUnit.getCivInfo().getMatchingUniques(bonusUniquePlaceholderText)) + bonusUniques.addAll(civUnit.getCivInfo().getMatchingUniques(UniqueType.KillUnitPlunder, stateForConditionals)) } - bonusUniquePlaceholderText = "Earn []% of [] unit's [] as [] when killed within 4 tiles of a city following this religion" val cityWithReligion = civUnit.getTile().getTilesInDistance(4).firstOrNull { - it.isCityCenter() && it.getCity()!!.getMatchingUniques(bonusUniquePlaceholderText).any() + it.isCityCenter() && it.getCity()!!.getLocalMatchingUniques(UniqueType.KillUnitPlunderNearCity, stateForConditionals).any() }?.getCity() if (cityWithReligion != null) { - bonusUniques.addAll(cityWithReligion.getLocalMatchingUniques(bonusUniquePlaceholderText)) + bonusUniques.addAll(cityWithReligion.getLocalMatchingUniques(UniqueType.KillUnitPlunderNearCity, stateForConditionals)) } for (unique in bonusUniques) { @@ -405,29 +403,43 @@ object Battle { // XP! private fun addXp(thisCombatant: ICombatant, amount: Int, otherCombatant: ICombatant) { + var baseXP = amount if (thisCombatant !is MapUnitCombatant) return if (thisCombatant.unit.promotions.totalXpProduced() >= thisCombatant.unit.civInfo.gameInfo.ruleSet.modOptions.maxXPfromBarbarians - && otherCombatant.getCivInfo().isBarbarian()) + && otherCombatant.getCivInfo().isBarbarian() + ) { return + } + + val stateForConditionals = StateForConditionals(civInfo = thisCombatant.getCivInfo(), ourCombatant = thisCombatant, theirCombatant = otherCombatant) + + for (unique in thisCombatant.getMatchingUniques(UniqueType.FlatXPGain, stateForConditionals, true)) + baseXP += unique.params[0].toInt() var xpModifier = 1f - for (unique in thisCombatant.getCivInfo().getMatchingUniques("[] units gain []% more Experience from combat")) { - if (thisCombatant.unit.matchesFilter(unique.params[0])) - xpModifier += unique.params[1].toFloat() / 100 - } - for (unique in thisCombatant.unit.getMatchingUniques("[]% Bonus XP gain")) + // Deprecated since 3.18.12 + for (unique in thisCombatant.getCivInfo().getMatchingUniques(UniqueType.BonusXPGainForUnits, stateForConditionals)) { + if (thisCombatant.unit.matchesFilter(unique.params[0])) + xpModifier += unique.params[1].toFloat() / 100 + } + for (unique in thisCombatant.getMatchingUniques(UniqueType.BonuxXPGain, stateForConditionals, true)) + xpModifier += unique.params[0].toFloat() / 100 + // + + for (unique in thisCombatant.getMatchingUniques(UniqueType.PercentageXPGain, stateForConditionals, true)) xpModifier += unique.params[0].toFloat() / 100 - - val xpGained = (amount * xpModifier).toInt() + + val xpGained = (baseXP * xpModifier).toInt() thisCombatant.unit.promotions.XP += xpGained if (thisCombatant.getCivInfo().isMajorCiv() && !otherCombatant.getCivInfo().isBarbarian()) { // Can't get great generals from Barbarians var greatGeneralPointsModifier = 1f - for (unique in thisCombatant.getMatchingUniques("[] is earned []% faster")) { + for (unique in thisCombatant.getMatchingUniques(UniqueType.GreatPersonEarnedFaster, stateForConditionals, true)) { val unitName = unique.params[0] - val unit = thisCombatant.getCivInfo().gameInfo.ruleSet.units[unitName] - if (unit != null && unit.uniques.contains("Great Person - [War]")) + // From the unique we know this unit exists + val unit = thisCombatant.getCivInfo().gameInfo.ruleSet.units[unitName]!! + if (unit.uniques.contains("Great Person - [War]")) greatGeneralPointsModifier += unique.params[1].toFloat() / 100 } @@ -438,7 +450,8 @@ object Battle { private fun conquerCity(city: CityInfo, attacker: MapUnitCombatant) { val attackerCiv = attacker.getCivInfo() - + + attackerCiv.addNotification("We have conquered the city of [${city.name}]!", city.location, NotificationIcon.War) city.hasJustBeenConquered = true @@ -448,7 +461,8 @@ object Battle { for (airUnit in airUnits.toList()) airUnit.destroy() } - for (unique in attacker.getMatchingUniques("Upon capturing a city, receive [] times its [] production as [] immediately")) { + val stateForConditionals = StateForConditionals(civInfo = attackerCiv, unit = attacker.unit, ourCombatant = attacker, attackedTile = city.getCenterTile()) + for (unique in attacker.getMatchingUniques(UniqueType.CaptureCityPlunder, stateForConditionals, true)) { attackerCiv.addStat( Stat.valueOf(unique.params[2]), unique.params[0].toInt() * city.cityStats.currentCityStats[Stat.valueOf(unique.params[1])].toInt() @@ -548,9 +562,10 @@ object Battle { fun mayUseNuke(nuke: MapUnitCombatant, targetTile: TileInfo): Boolean { val blastRadius = - if (!nuke.unit.hasUnique(UniqueType.BlastRadius)) 2 + if (!nuke.hasUnique(UniqueType.BlastRadius)) 2 + // Don't check conditionals as these are not supported else nuke.unit.getMatchingUniques(UniqueType.BlastRadius).first().params[0].toInt() - + var canNuke = true val attackerCiv = nuke.getCivInfo() for (tile in targetTile.getTilesInDistance(blastRadius)) { @@ -582,7 +597,8 @@ object Battle { } val blastRadius = - if (!attacker.unit.hasUnique(UniqueType.BlastRadius)) 2 + if (!attacker.hasUnique(UniqueType.BlastRadius)) 2 + // Don't check conditionals as there are not supported else attacker.unit.getMatchingUniques(UniqueType.BlastRadius).first().params[0].toInt() val strength = when { diff --git a/core/src/com/unciv/logic/battle/BattleDamage.kt b/core/src/com/unciv/logic/battle/BattleDamage.kt index 2e17662bf5..371ee9d09a 100644 --- a/core/src/com/unciv/logic/battle/BattleDamage.kt +++ b/core/src/com/unciv/logic/battle/BattleDamage.kt @@ -42,14 +42,13 @@ object BattleDamage { combatAction = combatAction, attackedTile = attackedTile ) - for (unique in combatant.unit.getMatchingUniques( + for (unique in combatant.getMatchingUniques( UniqueType.Strength, conditionalState, true )) { modifiers.add(getModifierStringFromUnique(unique), unique.params[0].toInt()) } - for (unique in combatant.unit.getMatchingUniques( - UniqueType.StrengthNearCapital, - checkCivInfoUniques = true + for (unique in combatant.getMatchingUniques( + UniqueType.StrengthNearCapital, conditionalState, true )) { if (civInfo.cities.isEmpty()) break val distance = diff --git a/core/src/com/unciv/logic/battle/MapUnitCombatant.kt b/core/src/com/unciv/logic/battle/MapUnitCombatant.kt index 56bc9c6c5c..931ac3736c 100644 --- a/core/src/com/unciv/logic/battle/MapUnitCombatant.kt +++ b/core/src/com/unciv/logic/battle/MapUnitCombatant.kt @@ -4,7 +4,9 @@ import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.MapUnit import com.unciv.logic.map.TileInfo import com.unciv.models.UncivSound +import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.Unique +import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unit.UnitType class MapUnitCombatant(val unit: MapUnit) : ICombatant { @@ -44,6 +46,10 @@ class MapUnitCombatant(val unit: MapUnit) : ICombatant { return unit.name+" of "+unit.civInfo.civName } - fun getMatchingUniques(uniqueTemplate: String): Sequence = unit.getMatchingUniques(uniqueTemplate) + fun getMatchingUniques(uniqueType: UniqueType, conditionalState: StateForConditionals, checkCivUniques: Boolean): Sequence = + unit.getMatchingUniques(uniqueType, conditionalState, checkCivUniques) + fun hasUnique(uniqueType: UniqueType, conditionalState: StateForConditionals? = null): Boolean = + if (conditionalState == null) unit.hasUnique(uniqueType) + else unit.hasUnique(uniqueType, conditionalState) } \ No newline at end of file diff --git a/core/src/com/unciv/logic/city/CityInfo.kt b/core/src/com/unciv/logic/city/CityInfo.kt index 5449ff0399..37ae8238f8 100644 --- a/core/src/com/unciv/logic/city/CityInfo.kt +++ b/core/src/com/unciv/logic/city/CityInfo.kt @@ -425,8 +425,9 @@ class CityInfo { buildingsCounter.add(building.greatPersonPoints) sourceToGPP["Buildings"] = buildingsCounter + val stateForConditionals = StateForConditionals(civInfo = civInfo, cityInfo = this) for ((_, gppCounter) in sourceToGPP) { - for (unique in civInfo.getMatchingUniques("[] is earned []% faster")) { + for (unique in civInfo.getMatchingUniques(UniqueType.GreatPersonEarnedFaster, stateForConditionals)) { val unitName = unique.params[0] if (!gppCounter.containsKey(unitName)) continue gppCounter.add(unitName, gppCounter[unitName]!! * unique.params[1].toInt() / 100) @@ -440,9 +441,8 @@ class CityInfo { // Sweden UP for (otherCiv in civInfo.getKnownCivs()) { - if (!civInfo.getDiplomacyManager(otherCiv) - .hasFlag(DiplomacyFlags.DeclarationOfFriendship) - ) continue + if (!civInfo.getDiplomacyManager(otherCiv).hasFlag(DiplomacyFlags.DeclarationOfFriendship)) + continue for (ourUnique in civInfo.getMatchingUniques("When declaring friendship, both parties gain a []% boost to great person generation")) allGppPercentageBonus += ourUnique.params[0].toInt() diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt index d9c6a03d15..0b2fc2264f 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt @@ -60,6 +60,15 @@ enum class UniqueParameterType(val parameterName:String) { return UniqueType.UniqueComplianceErrorSeverity.WarningOnly } }, + GreatPerson("greatPerson") { + override fun getErrorSeverity( + parameterText: String, + ruleset: Ruleset + ): UniqueType.UniqueComplianceErrorSeverity? { + return if (parameterText in ruleset.units && ruleset.units[parameterText]!!.hasUnique("Great Person - []")) null + else UniqueType.UniqueComplianceErrorSeverity.RulesetSpecific + } + }, Stats("stats") { override fun getErrorSeverity(parameterText: String, ruleset: Ruleset): UniqueType.UniqueComplianceErrorSeverity? { @@ -265,6 +274,16 @@ enum class UniqueParameterType(val parameterName:String) { else UniqueType.UniqueComplianceErrorSeverity.RulesetInvariant } }, + CostOrStrength("costOrStrength") { + private val knownValues = setOf("Cost", "Strength") + override fun getErrorSeverity( + parameterText: String, + ruleset: Ruleset + ): UniqueType.UniqueComplianceErrorSeverity? { + return if (parameterText in knownValues) null + else UniqueType.UniqueComplianceErrorSeverity.RulesetInvariant + } + }, /** Behaves like [Unknown], but states explicitly the parameter is OK and its contents are ignored */ Comment("comment") { override fun getErrorSeverity(parameterText: String, ruleset: Ruleset): diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index b72ba5589c..d72812f2e1 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -223,7 +223,7 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget, val flags: CanSeeInvisibleUnits("Can see invisible [mapUnitFilter] units", UniqueTarget.Unit), Strength("[amount]% Strength", UniqueTarget.Unit, UniqueTarget.Global), - StrengthNearCapital("[amount]% Strength decreasing with distance from the capital", UniqueTarget.Unit), + StrengthNearCapital("[amount]% Strength decreasing with distance from the capital", UniqueTarget.Unit, UniqueTarget.Global), Movement("[amount] Movement", UniqueTarget.Unit, UniqueTarget.Global), @@ -244,8 +244,16 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget, val flags: CarryExtraAirUnits("Can carry [amount] extra [mapUnitFilter] units", UniqueTarget.Unit), CannotBeCarriedBy("Cannot be carried by [mapUnitFilter] units", UniqueTarget.Unit), - UnitMaintenanceDiscount("[amount]% maintenance costs", UniqueTarget.Unit), - + UnitMaintenanceDiscount("[amount]% maintenance costs", UniqueTarget.Unit, UniqueTarget.Global), + GreatPersonEarnedFaster("[greatPerson] is earned [amount]% faster", UniqueTarget.Unit, UniqueTarget.Global), + + CaptureCityPlunder("Upon capturing a city, receive [amount] times its [stat] production as [stat] immediately", UniqueTarget.Unit, UniqueTarget.Global), + KillUnitPlunder("Earn [amount]% of killed [mapUnitFilter] unit's [costOrStrength] as [stat]", UniqueTarget.Unit, UniqueTarget.Global), + KillUnitPlunderNearCity("Earn [amount]% of [mapUnitFilter] unit's [costOrStrength] as [stat] when killed within 4 tiles of a city following this religion", UniqueTarget.FollowerBelief), + + FlatXPGain("[amount] XP gained from combat", UniqueTarget.Unit, UniqueTarget.Global), + PercentageXPGain("[amount]% XP gained from combat", UniqueTarget.Unit, UniqueTarget.Global), + // The following block gets cached in MapUnit for faster getMovementCostBetweenAdjacentTiles DoubleMovementOnTerrain("Double movement in [terrainFilter]", UniqueTarget.Unit), AllTilesCost1Move("All tiles cost 1 movement", UniqueTarget.Unit), @@ -554,7 +562,11 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget, val flags: @Deprecated("As of 3.16.16 - removed 3.17.11", ReplaceWith("[stats] "), DeprecationLevel.ERROR) StatBonusForNumberOfSpecialists("[stats] if this city has at least [amount] specialists", UniqueTarget.Global), - + @Deprecated("As of 3.18.12", ReplaceWith("[amount]% XP gained from combat")) + BonuxXPGain("[amount]% Bonus XP gain", UniqueTarget.Unit), + @Deprecated("As of 3.18.12", ReplaceWith("[amount]% XP gained from combat ")) + BonusXPGainForUnits("[mapUnitFilter] units gain [amount]% more Experience from combat", UniqueTarget.Global), + // endregion ; diff --git a/docs/uniques.md b/docs/uniques.md index 0c3729200c..722e5f60fe 100644 --- a/docs/uniques.md +++ b/docs/uniques.md @@ -220,6 +220,11 @@ Example: "[20]% Strength" Applicable to: Global, Unit +#### [amount]% Strength decreasing with distance from the capital +Example: "[20]% Strength decreasing with distance from the capital" + +Applicable to: Global, Unit + #### [amount] Movement Example: "[20] Movement" @@ -238,6 +243,36 @@ Applicable to: Global, Unit #### Normal vision when embarked Applicable to: Global, Unit +#### [amount]% maintenance costs +Example: "[20]% maintenance costs" + +Applicable to: Global, Unit + +#### [greatPerson] is earned [amount]% faster +Example: "[greatPerson] is earned [20]% faster" + +Applicable to: Global, Unit + +#### Upon capturing a city, receive [amount] times its [stat] production as [stat] immediately +Example: "Upon capturing a city, receive [20] times its [Culture] production as [Culture] immediately" + +Applicable to: Global, Unit + +#### Earn [amount]% of killed [mapUnitFilter] unit's [costOrStrength] as [stat] +Example: "Earn [20]% of killed [Wounded] unit's [costOrStrength] as [Culture]" + +Applicable to: Global, Unit + +#### [amount] XP gained from combat +Example: "[20] XP gained from combat" + +Applicable to: Global, Unit + +#### [amount]% XP gained from combat +Example: "[20]% XP gained from combat" + +Applicable to: Global, Unit + #### Free [baseUnitFilter] appears Example: "Free [Melee] appears" @@ -337,6 +372,11 @@ Example: "[20]% [Culture] from every follower, up to [20]%" Applicable to: FollowerBelief +#### Earn [amount]% of [mapUnitFilter] unit's [costOrStrength] as [stat] when killed within 4 tiles of a city following this religion +Example: "Earn [20]% of [Wounded] unit's [costOrStrength] as [Culture] when killed within 4 tiles of a city following this religion" + +Applicable to: FollowerBelief + ## Building uniques #### Remove extra unhappiness from annexed cities Applicable to: Building @@ -450,11 +490,6 @@ Example: "Can see invisible [Wounded] units" Applicable to: Unit -#### [amount]% Strength decreasing with distance from the capital -Example: "[20]% Strength decreasing with distance from the capital" - -Applicable to: Unit - #### May found a religion Applicable to: Unit @@ -490,11 +525,6 @@ Example: "Cannot be carried by [Wounded] units" Applicable to: Unit -#### [amount]% maintenance costs -Example: "[20]% maintenance costs" - -Applicable to: Unit - #### Double movement in [terrainFilter] Example: "Double movement in [Grassland]" @@ -1051,6 +1081,7 @@ Applicable to: Conditional - "+[amount]% Production when constructing [constructionFilter] [cityFilter]" - Deprecated As of 3.17.10 - removed 3.18.5, replace with "[amount]% Production when constructing [buildingFilter] buildings [cityFilter]" - "[stats] from every specialist" - Deprecated As of 3.16.16 - removed 3.17.11, replace with "[stats] from every specialist [in all cities]" - "[stats] if this city has at least [amount] specialists" - Deprecated As of 3.16.16 - removed 3.17.11, replace with "[stats] " + - "[mapUnitFilter] units gain [amount]% more Experience from combat" - Deprecated As of 3.18.12, replace with "[amount]% XP gained from combat " - "Not displayed as an available construction unless [buildingName] is built" - Deprecated As of 3.16.11, replace with "Not displayed as an available construction without [buildingName]" - "[stats] once [tech] is discovered" - Deprecated As of 3.17.10, replace with "[stats] " - "Double movement in coast" - Deprecated As of 3.17.1 - removed 3.17.13, replace with "Double movement in [terrainFilter]" @@ -1068,5 +1099,6 @@ Applicable to: Conditional - "+[amount]% Strength in [tileFilter]" - Deprecated As of 3.17.5 - removed 3.18.5, replace with "[amount]% Strength " - "[amount] Visibility Range" - Deprecated As of 3.17.5 - removed 3.18.5, replace with "[amount] Sight" - "Limited Visibility" - Deprecated As of 3.17.5 - removed 3.18.5, replace with "[-1] Sight" + - "[amount]% Bonus XP gain" - Deprecated As of 3.18.12, replace with "[amount]% XP gained from combat" - "[stats] on [tileFilter] tiles once [tech] is discovered" - Deprecated As of 3.17.10, replace with "[stats] from [tileFilter] tiles " - "Deal 30 damage to adjacent enemy units" - Deprecated As of 3.17.10, replace with "Adjacent enemy units ending their turn take [30] damage" \ No newline at end of file