diff --git a/android/assets/jsons/Civ V - Vanilla/Policies.json b/android/assets/jsons/Civ V - Vanilla/Policies.json index b2717f3920..47a6889629 100644 --- a/android/assets/jsons/Civ V - Vanilla/Policies.json +++ b/android/assets/jsons/Civ V - Vanilla/Policies.json @@ -360,33 +360,33 @@ }, { "name": "Universal Suffrage", - "uniques": ["[+1 Production] per [5] population in all cities"], + "uniques": ["+[33]% Defensive Strength for cities"], "row": 1, "column": 3 }, { "name": "Civil Society", - "uniques": ["-50% food consumption by specialists"], + "uniques": ["-[50]% food consumption by specialists"], "row": 1, "column": 5 }, { "name": "Free Speech", - "uniques": ["[+1 Culture] per [2] population in all cities"], + "uniques": ["[8] units cost no maintenance"], "requires": ["Constitution"], "row": 2, "column": 1 }, { "name": "Democracy", - "uniques": ["Specialists produce half normal unhappiness"], + "uniques": ["Specialists only produce [50]% of normal unhappiness"], "requires": ["Civil Society"], "row": 2, "column": 5 }, { "name": "Freedom Complete", - "uniques": ["Tile yield from Great Improvements +100%", "Golden Age length increased by [50]%"] + "uniques": ["+[100]% yield from [Great Improvements]", "Golden Age length increased by [50]%"] } ] }, diff --git a/core/src/com/unciv/logic/battle/BattleDamage.kt b/core/src/com/unciv/logic/battle/BattleDamage.kt index e0a1a0cdb7..50b4928404 100644 --- a/core/src/com/unciv/logic/battle/BattleDamage.kt +++ b/core/src/com/unciv/logic/battle/BattleDamage.kt @@ -167,50 +167,57 @@ object BattleDamage { return modifiers } - fun getDefenceModifiers(attacker: ICombatant, defender: MapUnitCombatant): Counter { + fun getDefenceModifiers(attacker: ICombatant, defender: ICombatant): Counter { val modifiers = getGeneralModifiers(defender, attacker) val tile = defender.getTile() + + if (defender is MapUnitCombatant) { - if (defender.unit.isEmbarked()) { - // embarked units get no defensive modifiers apart from this unique - if (defender.unit.hasUnique("Defense bonus when embarked") || - defender.getCivInfo().hasUnique("Embarked units can defend themselves") + if (defender.unit.isEmbarked()) { + // embarked units get no defensive modifiers apart from this unique + if (defender.unit.hasUnique("Defense bonus when embarked") || + defender.getCivInfo().hasUnique("Embarked units can defend themselves") + ) + modifiers["Embarked"] = 100 + + return modifiers + } + + modifiers.putAll(getTileSpecificModifiers(defender, tile)) + + val tileDefenceBonus = tile.getDefensiveBonus() + if (!defender.unit.hasUnique("No defensive terrain bonus") && tileDefenceBonus > 0 + || !defender.unit.hasUnique("No defensive terrain penalty") && tileDefenceBonus < 0 ) - modifiers["Embarked"] = 100 + modifiers["Tile"] = (tileDefenceBonus * 100).toInt() - return modifiers + if (attacker.isRanged()) { + val defenceVsRanged = 25 * defender.unit.getUniques() + .count { it.text == "+25% Defence against ranged attacks" } + if (defenceVsRanged > 0) modifiers["defence vs ranged"] = defenceVsRanged + } + + for (unique in defender.unit.getMatchingUniques("+[]% Strength when defending")) { + modifiers.add("Defender Bonus", unique.params[0].toInt()) + } + + for (unique in defender.unit.getMatchingUniques("+[]% defence in [] tiles")) { + if (tile.matchesUniqueFilter(unique.params[1])) + modifiers["[${unique.params[1]}] defence"] = unique.params[0].toInt() + } + + if (defender.unit.isFortified()) + modifiers["Fortification"] = 20 * defender.unit.getFortificationTurns() + } else if (defender is CityCombatant) { + + modifiers["Defensive Bonus"] = defender.city.civInfo.getMatchingUniques("+[]% Defensive strength for cities") + .map { it.params[0].toFloat() / 100f }.sum().toInt() + } - modifiers.putAll(getTileSpecificModifiers(defender, tile)) - - val tileDefenceBonus = tile.getDefensiveBonus() - if (!defender.unit.hasUnique("No defensive terrain bonus") && tileDefenceBonus > 0 - || !defender.unit.hasUnique("No defensive terrain penalty") && tileDefenceBonus < 0 - ) - modifiers["Tile"] = (tileDefenceBonus * 100).toInt() - - if (attacker.isRanged()) { - val defenceVsRanged = 25 * defender.unit.getUniques() - .count { it.text == "+25% Defence against ranged attacks" } - if (defenceVsRanged > 0) modifiers["defence vs ranged"] = defenceVsRanged - } - - for (unique in defender.unit.getMatchingUniques("+[]% Strength when defending")) { - modifiers.add("Defender Bonus", unique.params[0].toInt()) - } - - for (unique in defender.unit.getMatchingUniques("+[]% defence in [] tiles")) { - if (tile.matchesUniqueFilter(unique.params[1])) - modifiers["[${unique.params[1]}] defence"] = unique.params[0].toInt() - } - - - if (defender.unit.isFortified()) - modifiers["Fortification"] = 20 * defender.unit.getFortificationTurns() - return modifiers } - + private fun getTileSpecificModifiers(unit: MapUnitCombatant, tile: TileInfo): Counter { val modifiers = Counter() @@ -269,8 +276,7 @@ object BattleDamage { tileToAttackFrom: TileInfo?, defender: ICombatant ): Float { - val attackModifier = - modifiersToMultiplicationBonus(getAttackModifiers(attacker, defender)) + val attackModifier = modifiersToMultiplicationBonus(getAttackModifiers(attacker, defender)) return attacker.getAttackingStrength() * attackModifier } @@ -279,9 +285,7 @@ object BattleDamage { * Includes defence modifiers */ private fun getDefendingStrength(attacker: ICombatant, defender: ICombatant): Float { - var defenceModifier = 1f - if (defender is MapUnitCombatant) defenceModifier = - modifiersToMultiplicationBonus(getDefenceModifiers(attacker, defender)) + val defenceModifier = modifiersToMultiplicationBonus(getDefenceModifiers(attacker, defender)) return defender.getDefendingStrength() * defenceModifier } diff --git a/core/src/com/unciv/logic/city/CityStats.kt b/core/src/com/unciv/logic/city/CityStats.kt index 2333cbb35c..c43a830874 100644 --- a/core/src/com/unciv/logic/city/CityStats.kt +++ b/core/src/com/unciv/logic/city/CityStats.kt @@ -174,15 +174,24 @@ class CityStats { if (!civInfo.isPlayerCivilization()) unhappinessModifier *= civInfo.gameInfo.getDifficulty().aiUnhappinessModifier - var unhappinessFromCity = -3f // -3 happiness per city + var unhappinessFromCity = -3f // -3 happiness per city if (civInfo.hasUnique("Unhappiness from number of Cities doubled")) - unhappinessFromCity *= 2f//doubled for the Indian + unhappinessFromCity *= 2f //doubled for the Indian newHappinessList["Cities"] = unhappinessFromCity * unhappinessModifier var unhappinessFromCitizens = cityInfo.population.population.toFloat() - if (civInfo.hasUnique("Specialists produce half normal unhappiness")) - unhappinessFromCitizens -= cityInfo.population.getNumberOfSpecialists() * 0.5f + var unhappinessFromSpecialists = cityInfo.population.getNumberOfSpecialists().toFloat() + + for (unique in civInfo.getMatchingUniques("Specialists only produce []% of normal unhappiness")) { + unhappinessFromSpecialists *= (1f - unique.params[0].toFloat() / 100f) + } + // Deprecated since 3.15 + if (civInfo.hasUnique("Specialists produce half normal unhappiness")) + unhappinessFromSpecialists *= 0.5f + // + + unhappinessFromCitizens -= cityInfo.population.getNumberOfSpecialists().toFloat() - unhappinessFromSpecialists if (cityInfo.isPuppet) unhappinessFromCitizens *= 1.5f @@ -297,8 +306,7 @@ class CityStats { for (unique in uniques.filter { it.placeholderText == "+[]% [] []"}) if (cityInfo.matchesFilter(unique.params[2])) stats.add(Stat.valueOf(unique.params[1]), unique.params[0].toFloat()) - - + for (unique in uniques.filter { it.placeholderText == "+[]% Production when constructing []" }) { if (constructionMatchesFilter(currentConstruction, unique.params[1])) stats.production += unique.params[0].toInt() @@ -391,7 +399,7 @@ class CityStats { fun update(currentConstruction: IConstruction = cityInfo.cityConstructions.getCurrentConstruction()) { val citySpecificUniques: Sequence = cityInfo.cityConstructions.builtBuildingUniqueMap.getAllUniques() - .filter { it.params.isNotEmpty() && it.params.last()=="in this city" } + .filter { it.params.isNotEmpty() && it.params.last() == "in this city" } // We need to compute Tile yields before happiness updateBaseStatList() updateCityHappiness() @@ -505,8 +513,16 @@ class CityStats { private fun updateFoodEaten() { foodEaten = cityInfo.population.population.toFloat() * 2 - if (cityInfo.civInfo.hasUnique("-50% food consumption by specialists")) - foodEaten -= cityInfo.population.getNumberOfSpecialists() + var foodEatenBySpecialists = 2f * cityInfo.population.getNumberOfSpecialists() + + for (unique in cityInfo.civInfo.getMatchingUniques("-[]% food consumption by specialists")) + foodEatenBySpecialists *= 1f - unique.params[0].toFloat() / 100f + + // Deprecated since 3.15 + if (cityInfo.civInfo.hasUnique("-50% food consumption by specialists")) + foodEatenBySpecialists *= 0.5f + // + + foodEaten -= 2f * cityInfo.population.getNumberOfSpecialists() - foodEatenBySpecialists } - -} \ No newline at end of file +} diff --git a/core/src/com/unciv/logic/civilization/CivInfoStats.kt b/core/src/com/unciv/logic/civilization/CivInfoStats.kt index 742127ec2f..b26a441fcc 100644 --- a/core/src/com/unciv/logic/civilization/CivInfoStats.kt +++ b/core/src/com/unciv/logic/civilization/CivInfoStats.kt @@ -16,7 +16,11 @@ class CivInfoStats(val civInfo: CivilizationInfo) { private fun getUnitMaintenance(): Int { val baseUnitCost = 0.5f - val freeUnits = 3 + var freeUnits = 3 + for (unique in civInfo.getMatchingUniques("[] units cost no maintenance")) { + freeUnits += unique.params[0].toInt() + } + var unitsToPayFor = civInfo.getCivUnits() if (civInfo.hasUnique("Units in cities cost no Maintenance")) // Only land military units can truly "garrison" @@ -39,7 +43,15 @@ class CivInfoStats(val civInfo: CivilizationInfo) { cost = cost.pow(1 + gameProgress / 3) // Why 3? To spread 1 to 1.33 if (!civInfo.isPlayerCivilization()) cost *= civInfo.gameInfo.getDifficulty().aiUnitMaintenanceModifier - if (civInfo.hasUnique("-33% unit upkeep costs")) cost *= 0.66f + + for (unique in civInfo.getMatchingUniques("-[]% unit upkeep costs")) { + cost *= 1f - unique.params[0].toFloat() / 100f + } + + // Deprecated since 3.15 + if (civInfo.hasUnique("-33% unit upkeep costs")) cost *= 0.67f + // + return cost.toInt() } diff --git a/core/src/com/unciv/logic/map/TileInfo.kt b/core/src/com/unciv/logic/map/TileInfo.kt index 50249e1c50..6d0bcfa26a 100644 --- a/core/src/com/unciv/logic/map/TileInfo.kt +++ b/core/src/com/unciv/logic/map/TileInfo.kt @@ -286,24 +286,21 @@ open class TileInfo { if (city != null) { val cityWideUniques = city.cityConstructions.builtBuildingUniqueMap.getUniques("[] from [] tiles in this city") - val civWideUniques = city.civInfo.getMatchingUniques("[] from every []") val improvementUniques = improvement.uniqueObjects.filter { it.placeholderText == "[] on [] tiles once [] is discovered" && observingCiv.tech.isResearched(it.params[2]) } - for (unique in cityWideUniques + civWideUniques + improvementUniques) { - if (improvement.name == unique.params[1] - || unique.params[1] == "Great Improvement" && improvement.isGreatImprovement() - || unique.params[1] == "Fresh water" && isAdjacentToFreshwater - || unique.params[1] == "non-fresh water" && !isAdjacentToFreshwater - ) + for (unique in cityWideUniques + improvementUniques) { + if (matchesUniqueFilter(unique.params[1])) stats.add(unique.stats) } - } - if (containsGreatImprovement() - && observingCiv.hasUnique("Tile yield from Great Improvements +100%")) - stats.add(improvement) // again, for the double effect + for (unique in city.civInfo.getMatchingUniques("[] from every []")) { + if (improvement.matchesFilter(unique.params[1])) { + stats.add(unique.stats) + } + } + } for (unique in improvement.uniqueObjects) if (unique.placeholderText == "[] for each adjacent []") { @@ -315,7 +312,16 @@ open class TileInfo { } stats.add(unique.stats.times(numberOfBonuses.toFloat())) } - + + for (unique in observingCiv.getMatchingUniques("+[]% yield from []")) + if (improvement.matchesFilter(unique.params[0])) + stats.timesInPlace(1f + unique.params[1].toFloat() / 100f) + + // Deprecated since 3.15 + if (containsGreatImprovement() && observingCiv.hasUnique("Tile yield from Great Improvements +100%")) + stats.timesInPlace(2f) + // + return stats } @@ -386,23 +392,28 @@ open class TileInfo { * Implementation of _`tileFilter`_ * @see tileFilter */ - fun matchesUniqueFilter(filter: String, civInfo: CivilizationInfo? = null): Boolean { - return filter == "All" - || filter == baseTerrain - || filter == "River" && isAdjacentToRiver() - || terrainFeatures.contains(filter) - || baseTerrainObject.uniques.contains(filter) - || improvement == filter - || resource == filter - || resource != null && getTileResource().resourceType.name + " resource" == filter - || filter == "Water" && isWater - || filter == "Land" && isLand - || filter == "Coastal" && isCoastalTile() - || filter == naturalWonder - || terrainFeatures.isNotEmpty() && getTerrainFeatures().last().uniques.contains(filter) - || civInfo != null && hasViewableResource(civInfo) && resource == filter - || filter == "Foreign Land" && civInfo != null && !isFriendlyTerritory(civInfo) - || filter == "Friendly Land" && civInfo != null && isFriendlyTerritory(civInfo) + fun matchesUniqueFilter(filter: String, civInfo: CivilizationInfo? = null): Boolean { + return when (filter) { + "All" -> true + "Water" -> isWater + "Land" -> isLand + "Coastal" -> isCoastalTile() + "River" -> isAdjacentToRiver() + "Fresh water" -> isAdjacentToFreshwater + "non-fresh water" -> !isAdjacentToFreshwater + improvement -> true + naturalWonder -> true + "Foreign Land" -> civInfo != null && !isFriendlyTerritory(civInfo) + "Friendly Land" -> civInfo != null && isFriendlyTerritory(civInfo) + else -> { + if (terrainFeatures.contains(filter)) return true + if (baseTerrainObject.uniques.contains(filter)) return true + if (terrainFeatures.isNotEmpty() && getTerrainFeatures().last().uniques.contains(filter)) return true + if (resource != null && getTileResource().resourceType.name + " resource" == filter) return true + if (civInfo != null && hasViewableResource(civInfo) && resource == filter) return true + return false + } + } } fun hasImprovementInProgress() = improvementInProgress != null diff --git a/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt b/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt index 435976ec1c..14839b1918 100644 --- a/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt +++ b/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt @@ -94,5 +94,14 @@ class TileImprovement : NamedStats() { && it.params[0] == name }.any() } + + fun matchesFilter(filter: String): Boolean { + return when (filter) { + name -> true + "All" -> true + "Great Improvement" -> isGreatImprovement() + else -> false + } + } } diff --git a/core/src/com/unciv/models/stats/Stats.kt b/core/src/com/unciv/models/stats/Stats.kt index df0511baab..1be0b5ddb9 100644 --- a/core/src/com/unciv/models/stats/Stats.kt +++ b/core/src/com/unciv/models/stats/Stats.kt @@ -64,6 +64,12 @@ open class Stats() { for (stat in Stat.values()) hashMap[stat] = number * hashMap[stat]!! return Stats(hashMap) } + + fun timesInPlace(number: Float) { + val hashMap = toHashMap() + for (stat in Stat.values()) hashMap[stat] = number * hashMap[stat]!! + setStats(hashMap) + } fun isEmpty() = equals(Stats())