diff --git a/core/src/com/unciv/Constants.kt b/core/src/com/unciv/Constants.kt index a0c9b7b45a..3513ee6311 100644 --- a/core/src/com/unciv/Constants.kt +++ b/core/src/com/unciv/Constants.kt @@ -4,8 +4,8 @@ object Constants { const val worker = "Worker" const val settler = "Settler" const val eraSpecificUnit = "Era Starting Unit" - const val spreadReligionAbilityCount = "Spread Religion" - const val removeHeresyAbilityCount = "Remove Foreign religions from your own cities" + const val spreadReligion = "Spread Religion" + const val removeHeresy = "Remove Foreign religions from your own cities" const val impassable = "Impassable" const val ocean = "Ocean" @@ -85,6 +85,7 @@ object Constants { * _Most_ checks do compare to 0! */ const val minimumMovementEpsilon = 0.05f // 0.1f was used previously, too - here for global searches + const val aiPreferInquisitorOverMissionaryPressureDifference = 3000f const val defaultFontSize = 18 const val headingFontSize = 24 diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index bc6e7cd74b..6a73533c0d 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -12,7 +12,7 @@ import com.unciv.logic.BackwardCompatibility.removeMissingModReferences import com.unciv.logic.BackwardCompatibility.updateGreatGeneralUniques import com.unciv.logic.GameInfo.Companion.CURRENT_COMPATIBILITY_NUMBER import com.unciv.logic.GameInfo.Companion.FIRST_WITHOUT -import com.unciv.logic.automation.NextTurnAutomation +import com.unciv.logic.automation.civilization.NextTurnAutomation import com.unciv.logic.city.CityInfo import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.CivilizationInfoPreview diff --git a/core/src/com/unciv/logic/automation/ConstructionAutomation.kt b/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt similarity index 79% rename from core/src/com/unciv/logic/automation/ConstructionAutomation.kt rename to core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt index 71f4f64b28..9c695d618f 100644 --- a/core/src/com/unciv/logic/automation/ConstructionAutomation.kt +++ b/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt @@ -1,5 +1,7 @@ -package com.unciv.logic.automation +package com.unciv.logic.automation.city +import com.unciv.logic.automation.Automation +import com.unciv.logic.automation.civilization.NextTurnAutomation import com.unciv.logic.city.CityConstructions import com.unciv.logic.city.INonPerpetualConstruction import com.unciv.logic.city.PerpetualConstruction @@ -11,7 +13,6 @@ import com.unciv.models.ruleset.MilestoneType import com.unciv.models.ruleset.Victory import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.UniqueType -import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.stats.Stat import kotlin.math.max import kotlin.math.sqrt @@ -47,8 +48,6 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ private val relativeCostEffectiveness = ArrayList() - private val faithConstruction = arrayListOf() - private data class ConstructionChoice(val choice: String, var choiceModifier: Float, val remainingWork: Int) private fun addChoice(choices: ArrayList, choice: String, choiceModifier: Float) { @@ -79,7 +78,6 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ addWorkerChoice() addWorkBoatChoice() addMilitaryUnitChoice() - addReligiousUnit() } val production = cityInfo.cityStats.currentCityStats.production @@ -106,14 +104,6 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ NotificationIcon.Construction ) cityConstructions.currentConstructionFromQueue = chosenConstruction - - if (civInfo.isPlayerCivilization()) return // don't want the ai to control what a player uses faith for - - val chosenItem = faithConstruction.firstOrNull { - it.getStatBuyCost(cityInfo, stat = Stat.Faith)!! <= civInfo.religionManager.storedFaith - } ?: return - - cityConstructions.purchaseConstruction(chosenItem.name, -1, false, stat=Stat.Faith) } private fun addMilitaryUnitChoice() { @@ -189,9 +179,10 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ private fun addCultureBuildingChoice() { val cultureBuilding = nonWonders - .filter { it.isStatRelated(Stat.Culture) - && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } - .isBuildable().minByOrNull { it.cost } + .filter { it.isStatRelated(Stat.Culture) + && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) + }.isBuildable() + .minByOrNull { it.cost } if (cultureBuilding != null) { var modifier = 0.5f if (cityInfo.cityStats.currentCityStats.culture == 0f) // It won't grow if we don't help it @@ -266,9 +257,10 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ private fun addUnitTrainingBuildingChoice() { val unitTrainingBuilding = nonWonders - .filter { it.hasUnique(UniqueType.UnitStartingExperience) - && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } - .isBuildable().minByOrNull { it.cost } + .filter { it.hasUnique(UniqueType.UnitStartingExperience) + && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) + }.isBuildable() + .minByOrNull { it.cost } if (unitTrainingBuilding != null && (!civInfo.wantsToFocusOn(Victory.Focus.Culture) || isAtWar)) { var modifier = if (cityIsOverAverageProduction) 0.5f else 0.1f // You shouldn't be cranking out units anytime soon if (isAtWar) modifier *= 2 @@ -280,9 +272,10 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ private fun addDefenceBuildingChoice() { val defensiveBuilding = nonWonders - .filter { it.cityStrength > 0 - && Automation.allowAutomatedConstruction(civInfo, cityInfo, it)} - .isBuildable().minByOrNull { it.cost } + .filter { it.cityStrength > 0 + && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) + }.isBuildable() + .minByOrNull { it.cost } if (defensiveBuilding != null && (isAtWar || !civInfo.wantsToFocusOn(Victory.Focus.Culture))) { var modifier = 0.2f if (isAtWar) modifier = 0.5f @@ -299,10 +292,11 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ private fun addHappinessBuildingChoice() { val happinessBuilding = nonWonders - .filter { (it.isStatRelated(Stat.Happiness) - || it.uniques.contains("Remove extra unhappiness from annexed cities")) - && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } - .isBuildable().minByOrNull { it.cost } + .filter { (it.isStatRelated(Stat.Happiness) + || it.uniques.contains("Remove extra unhappiness from annexed cities")) + && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } + .isBuildable() + .minByOrNull { it.cost } if (happinessBuilding != null) { var modifier = 1f val civHappiness = civInfo.getHappiness() @@ -359,48 +353,4 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ addChoice(relativeCostEffectiveness, foodBuilding.name, modifier) } } - - private fun addReligiousUnit() { - // these 4 if conditions are used to determine if an AI should buy units to spread religion, or spend faith to buy things like new military units or new buildings. - // currently this AI can only buy inquisitors and missionaries with faith - // this system will have to be reengineered to support buying other stuff with faith - if (civInfo.wantsToFocusOn(Victory.Focus.Military)) return - if (civInfo.religionManager.religion?.name == null) return - if (cityInfo.religion.getMajorityReligion()?.name != civInfo.religionManager.religion?.name) - return // you don't want to build units of opposing religions. - - - var modifier = 0f - - // The performance of the regular getMatchingUniques is better, since it only tries to find one unique, - // while the canBePurchasedWithStat tries (at time of writing) *6* different uniques. - val missionary = units - .firstOrNull { it -> it.getMatchingUniques("Can [] [] times").any { it.params[0] == "Spread Religion"} - && it.canBePurchasedWithStat(cityInfo, Stat.Faith) } - - - val inquisitor = units - .firstOrNull { it.hasUnique("Prevents spreading of religion to the city it is next to") - && it.canBePurchasedWithStat(cityInfo, Stat.Faith) } - - - if (civInfo.wantsToFocusOn(Victory.Focus.Culture)) modifier += 1 - if (isAtWar) modifier -= 0.5f - - val citiesNotFollowingOurReligion = civInfo.cities.asSequence() - .filterNot { it.religion.getMajorityReligion()?.name == civInfo.religionManager.religion!!.name } - - val buildInquisitor = citiesNotFollowingOurReligion - .filter { it.religion.getMajorityReligion()?.name == civInfo.religionManager.religion?.name } - .toList().size.toFloat() / 10 + modifier - - val possibleSpreadReligionTargets = civInfo.gameInfo.getCities() - .filter { it.getCenterTile().aerialDistanceTo(cityInfo.getCenterTile()) < 30 } - - val buildMissionary = possibleSpreadReligionTargets.toList().size.toFloat() / 15 + modifier - - if (buildMissionary > buildInquisitor && missionary != null && missionary.isBuildable(cityConstructions)) faithConstruction.add(missionary) - else if (inquisitor != null && inquisitor.isBuildable(cityConstructions)) faithConstruction.add(inquisitor) - } - } diff --git a/core/src/com/unciv/logic/automation/BarbarianAutomation.kt b/core/src/com/unciv/logic/automation/civilization/BarbarianAutomation.kt similarity index 94% rename from core/src/com/unciv/logic/automation/BarbarianAutomation.kt rename to core/src/com/unciv/logic/automation/civilization/BarbarianAutomation.kt index 0b514f61cb..418490ef9b 100644 --- a/core/src/com/unciv/logic/automation/BarbarianAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/BarbarianAutomation.kt @@ -1,6 +1,8 @@ -package com.unciv.logic.automation +package com.unciv.logic.automation.civilization import com.unciv.Constants +import com.unciv.logic.automation.unit.BattleHelper +import com.unciv.logic.automation.unit.UnitAutomation import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.MapUnit @@ -62,4 +64,4 @@ class BarbarianAutomation(val civInfo: CivilizationInfo) { UnitAutomation.wander(unit) } -} \ No newline at end of file +} diff --git a/core/src/com/unciv/logic/automation/NextTurnAutomation.kt b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt similarity index 98% rename from core/src/com/unciv/logic/automation/NextTurnAutomation.kt rename to core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt index 0ee50a2c51..3b70893d7f 100644 --- a/core/src/com/unciv/logic/automation/NextTurnAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt @@ -1,6 +1,9 @@ -package com.unciv.logic.automation +package com.unciv.logic.automation.civilization import com.unciv.Constants +import com.unciv.logic.automation.Automation +import com.unciv.logic.automation.ThreatLevel +import com.unciv.logic.automation.unit.UnitAutomation import com.unciv.logic.battle.BattleDamage import com.unciv.logic.battle.CityCombatant import com.unciv.logic.battle.MapUnitCombatant @@ -54,11 +57,14 @@ object NextTurnAutomation { respondToTradeRequests(civInfo) if (civInfo.isMajorCiv()) { - if (!civInfo.gameInfo.ruleSet.modOptions.uniques.contains(ModOptionsConstants.diplomaticRelationshipsCannotChange)) { + if (!civInfo.gameInfo.ruleSet.modOptions.hasUnique(ModOptionsConstants.diplomaticRelationshipsCannotChange)) { declareWar(civInfo) offerPeaceTreaty(civInfo) // offerDeclarationOfFriendship(civInfo) } + if (civInfo.gameInfo.isReligionEnabled()) { + ReligionAutomation.spendFaithOnReligion(civInfo) + } offerResearchAgreement(civInfo) exchangeLuxuries(civInfo) issueRequests(civInfo) @@ -590,7 +596,7 @@ object NextTurnAutomation { .flatMap { religion -> religion.getBeliefs(beliefType) }.contains(it.value) } .map { it.value } - .maxByOrNull { ChooseBeliefsAutomation.rateBelief(civInfo, it) } + .maxByOrNull { ReligionAutomation.rateBelief(civInfo, it) } } private fun potentialLuxuryTrades(civInfo: CivilizationInfo, otherCivInfo: CivilizationInfo): ArrayList { diff --git a/core/src/com/unciv/logic/automation/ChooseBeliefsAutomation.kt b/core/src/com/unciv/logic/automation/civilization/ReligionAutomation.kt similarity index 52% rename from core/src/com/unciv/logic/automation/ChooseBeliefsAutomation.kt rename to core/src/com/unciv/logic/automation/civilization/ReligionAutomation.kt index f3b2e53f86..312849d077 100644 --- a/core/src/com/unciv/logic/automation/ChooseBeliefsAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/ReligionAutomation.kt @@ -1,6 +1,8 @@ -package com.unciv.logic.automation +package com.unciv.logic.automation.civilization +import com.unciv.Constants import com.unciv.logic.city.CityInfo +import com.unciv.logic.city.INonPerpetualConstruction import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.ReligionState import com.unciv.logic.map.TileInfo @@ -13,7 +15,181 @@ import kotlin.math.min import kotlin.math.pow import kotlin.random.Random -object ChooseBeliefsAutomation { +object ReligionAutomation { + + // region faith spending + + fun spendFaithOnReligion(civInfo: CivilizationInfo) { + if (civInfo.cities.isEmpty()) return + + // Save for great prophet + if (civInfo.religionManager.religionState != ReligionState.EnhancedReligion + && (civInfo.religionManager.remainingFoundableReligions() != 0 || civInfo.religionManager.religionState > ReligionState.Pantheon) + ) { + buyGreatProphetInAnyCity(civInfo) + return + } + + // We don't have a religion and no more change of getting it :( + if (civInfo.religionManager.religionState <= ReligionState.Pantheon) { + tryBuyAnyReligiousBuilding(civInfo) + // Todo: buy Great People post industrial era + return + } + + // If we don't have majority in all our own cities, build missionaries and inquisitors to solve this + val citiesWithoutOurReligion = civInfo.cities.filter { it.religion.getMajorityReligion() != civInfo.religionManager.religion!! } + // The original had a cap at 4 missionaries total, but 1/4 * the number of cities should be more appropriate imo + if (citiesWithoutOurReligion.count() > + 4 * civInfo.getCivUnits().count { it.canDoReligiousAction(Constants.spreadReligion) || it.canDoReligiousAction(Constants.removeHeresy) } + ) { + val (city, pressureDifference) = citiesWithoutOurReligion.map { city -> + city to city.religion.getPressureDeficit(civInfo.religionManager.religion?.name) + }.maxBy { it.second } + if (pressureDifference >= Constants.aiPreferInquisitorOverMissionaryPressureDifference) + buyInquisitorNear(civInfo, city) + buyMissionaryInAnyCity(civInfo) + return + } + + + // Get an inquisitor to defend our holy city + val holyCity = civInfo.religionManager.getHolyCity() + if (holyCity != null + && holyCity in civInfo.cities + && civInfo.getCivUnits().count { it.hasUnique(UniqueType.PreventSpreadingReligion) } == 0 + && !holyCity.religion.isProtectedByInquisitor() + ) { + buyInquisitorNear(civInfo, holyCity) + return + } + + // Buy religious buildings in cities if possible + val citiesWithMissingReligiousBuildings = civInfo.cities.filter { city -> + city.religion.getMajorityReligion() != null + && !city.cityConstructions.builtBuildings.containsAll(city.religion.getMajorityReligion()!!.buildingsPurchasableByBeliefs) + } + if (citiesWithMissingReligiousBuildings.any()) { + tryBuyAnyReligiousBuilding(civInfo) + return + } + + // Todo: buy Great People post industrial era + + // Just buy missionaries to spread our religion outside of our civ + if (civInfo.getCivUnits().count { it.canDoReligiousAction(Constants.spreadReligion) } < 4) { + buyMissionaryInAnyCity(civInfo) + return + } + // Todo: buy inquisitors for defence of other cities + } + + private fun tryBuyAnyReligiousBuilding(civInfo: CivilizationInfo) { + for (city in civInfo.cities) { + if (city.religion.getMajorityReligion() == null) continue + val buildings = city.religion.getMajorityReligion()!!.buildingsPurchasableByBeliefs + val buildingToBePurchased = buildings + .asSequence() + .map { civInfo.getEquivalentBuilding(it).name } + .map { city.cityConstructions.getConstruction(it) as INonPerpetualConstruction } + .filter { it.isPurchasable(city.cityConstructions) } + .filter { (it.getStatBuyCost(city, Stat.Faith) ?: return@filter false) <= civInfo.religionManager.storedFaith } + .minByOrNull { it.getStatBuyCost(city, Stat.Faith)!! } + ?: continue + city.cityConstructions.purchaseConstruction(buildingToBePurchased.name, -1, true, Stat.Faith) + return + } + } + + private fun buyMissionaryInAnyCity(civInfo: CivilizationInfo) { + if (civInfo.religionManager.religionState < ReligionState.Religion) return + var missionaries = civInfo.gameInfo.ruleSet.units.values.filter { unit -> + unit.getMatchingUniques(UniqueType.CanActionSeveralTimes).filter { it.params[0] == Constants.spreadReligion }.any() + }.map { it.name } + missionaries = missionaries.map { civInfo.getEquivalentUnit(it).name } + val missionaryConstruction = missionaries + .map { civInfo.cities.first().cityConstructions.getConstruction(it) as INonPerpetualConstruction } + .filter { unit -> civInfo.cities.any { unit.isPurchasable(it.cityConstructions) && unit.canBePurchasedWithStat(it, Stat.Faith) } } + .minByOrNull { it.getStatBuyCost(civInfo.getCapital()!!, Stat.Faith)!! } + ?: return + + + val hasUniqueToTakeCivReligion = civInfo.gameInfo.ruleSet.units[missionaryConstruction.name]!!.hasUnique(UniqueType.TakeReligionOverBirthCity) + + val validCitiesToBuy = civInfo.cities.filter { + (hasUniqueToTakeCivReligion || it.religion.getMajorityReligion() == civInfo.religionManager.religion) + && (missionaryConstruction.getStatBuyCost(it, Stat.Faith) ?: return@filter false) <= civInfo.religionManager.storedFaith + && missionaryConstruction.isPurchasable(it.cityConstructions) + && missionaryConstruction.canBePurchasedWithStat(it, Stat.Faith) + } + if (validCitiesToBuy.isEmpty()) return + + val citiesWithBonusCharges = validCitiesToBuy.filter { city -> + city.getLocalMatchingUniques(UniqueType.UnitStartingActions).filter { it.params[2] == Constants.spreadReligion }.any() + } + val holyCity = validCitiesToBuy.firstOrNull { it.religion.religionThisIsTheHolyCityOf == civInfo.religionManager.religion!!.name } + + val cityToBuyMissionary = when { + citiesWithBonusCharges.any() -> citiesWithBonusCharges.first() + holyCity != null -> holyCity + else -> validCitiesToBuy.first() + } + + cityToBuyMissionary.cityConstructions.purchaseConstruction(missionaryConstruction.name, -1, true, Stat.Faith) + return + } + + private fun buyGreatProphetInAnyCity(civInfo: CivilizationInfo) { + if (civInfo.religionManager.religionState < ReligionState.Religion) return + var greatProphetUnit = civInfo.religionManager.getGreatProphetEquivalent() ?: return + greatProphetUnit = civInfo.getEquivalentUnit(greatProphetUnit).name + val greatProphetConstruction = civInfo.cities.first().cityConstructions.getConstruction(greatProphetUnit) as INonPerpetualConstruction + val cityToBuyGreatProphet = civInfo.cities + .asSequence() + .filter { greatProphetConstruction.isPurchasable(it.cityConstructions) } + .filter { greatProphetConstruction.canBePurchasedWithStat(it, Stat.Faith) } + .filter { (greatProphetConstruction.getStatBuyCost(it, Stat.Faith) ?: return@filter false) <= civInfo.religionManager.storedFaith } + .minByOrNull { greatProphetConstruction.getStatBuyCost(it, Stat.Faith)!! } + ?: return + cityToBuyGreatProphet.cityConstructions.purchaseConstruction(greatProphetUnit, -1, true, Stat.Faith) + } + + private fun buyInquisitorNear(civInfo: CivilizationInfo, city: CityInfo) { + if (civInfo.religionManager.religionState < ReligionState.Religion) return + var inquisitors = civInfo.gameInfo.ruleSet.units.values.filter { + it.getMapUnit(civInfo).canDoReligiousAction(Constants.removeHeresy) + || it.hasUnique(UniqueType.PreventSpreadingReligion) + }.map { it.name } + inquisitors = inquisitors.map { civInfo.getEquivalentUnit(it).name } + val inquisitorConstruction = inquisitors + .map { civInfo.cities.first().cityConstructions.getConstruction(it) as INonPerpetualConstruction } + .filter { unit -> civInfo.cities.any { unit.isPurchasable(it.cityConstructions) } } + .minByOrNull { it.getStatBuyCost(civInfo.getCapital()!!, Stat.Faith)!! } + ?: return + + val hasUniqueToTakeCivReligion = civInfo.gameInfo.ruleSet.units[inquisitorConstruction.name]!!.hasUnique(UniqueType.TakeReligionOverBirthCity) + + val validCitiesToBuy = civInfo.cities.filter { + (hasUniqueToTakeCivReligion || it.religion.getMajorityReligion() == civInfo.religionManager.religion) + && (inquisitorConstruction.getStatBuyCost(it, Stat.Faith) ?: return@filter false) <= civInfo.religionManager.storedFaith + && inquisitorConstruction.isPurchasable(it.cityConstructions) + && inquisitorConstruction.canBePurchasedWithStat(it, Stat.Faith) + } + if (validCitiesToBuy.isEmpty()) return + + val cityToBuy = validCitiesToBuy + .filter { + inquisitorConstruction.isPurchasable(it.cityConstructions) + && inquisitorConstruction.canBePurchasedWithStat(it, Stat.Faith) + } + .minByOrNull { it.getCenterTile().aerialDistanceTo(city.getCenterTile()) } ?: return + + cityToBuy.cityConstructions.purchaseConstruction(inquisitorConstruction.name, -1, true, Stat.Faith) + } + + // endregion + + // region rate beliefs fun rateBelief(civInfo: CivilizationInfo, belief: Belief): Float { var score = 0f @@ -206,4 +382,6 @@ object ChooseBeliefsAutomation { return score } + + //endregion } diff --git a/core/src/com/unciv/logic/automation/BattleHelper.kt b/core/src/com/unciv/logic/automation/unit/BattleHelper.kt similarity index 99% rename from core/src/com/unciv/logic/automation/BattleHelper.kt rename to core/src/com/unciv/logic/automation/unit/BattleHelper.kt index 0199f4bc4a..8bf152d858 100644 --- a/core/src/com/unciv/logic/automation/BattleHelper.kt +++ b/core/src/com/unciv/logic/automation/unit/BattleHelper.kt @@ -1,4 +1,4 @@ -package com.unciv.logic.automation +package com.unciv.logic.automation.unit import com.unciv.Constants import com.unciv.logic.battle.Battle diff --git a/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/SpecificUnitAutomation.kt similarity index 81% rename from core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt rename to core/src/com/unciv/logic/automation/unit/SpecificUnitAutomation.kt index 661ef8b6cd..09831fc5e6 100644 --- a/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/SpecificUnitAutomation.kt @@ -1,5 +1,7 @@ -package com.unciv.logic.automation +package com.unciv.logic.automation.unit +import com.unciv.Constants +import com.unciv.logic.automation.Automation import com.unciv.logic.battle.Battle import com.unciv.logic.battle.GreatGeneralImplementation import com.unciv.logic.battle.MapUnitCombatant @@ -114,7 +116,7 @@ object SpecificUnitAutomation { val tileForCitadel = cityToGarrison.getTilesInDistanceRange(3..4) .firstOrNull { reachableTest(it) && - WorkerAutomation.evaluateFortPlacement(it, unit.civInfo, true) + WorkerAutomation.evaluateFortPlacement(it, unit.civInfo, true) } if (tileForCitadel == null) { unit.movement.headTowards(cityToGarrison) @@ -271,7 +273,12 @@ object SpecificUnitAutomation { } // if we got here, we're pretty close, start looking! - val chosenTile = applicableTiles.sortedByDescending { Automation.rankTile(it, unit.civInfo) } + val chosenTile = applicableTiles.sortedByDescending { + Automation.rankTile( + it, + unit.civInfo + ) + } .firstOrNull { unit.movement.canReach(it) } ?: continue // to another city @@ -294,78 +301,105 @@ object SpecificUnitAutomation { } fun automateMissionary(unit: MapUnit) { - if (unit.religion != unit.civInfo.religionManager.religion?.name) - return unit.destroy() + if (unit.religion != unit.civInfo.religionManager.religion?.name || unit.religion == null) + return unit.disband() - val city = unit.civInfo.gameInfo.getCities().asSequence() - .filter { it.religion.getMajorityReligion()?.name != unit.getReligionDisplayName() } - .filter { it.civInfo.knows(unit.civInfo) && !it.civInfo.isAtWarWith(unit.civInfo) } - .filterNot { it.religion.isProtectedByInquisitor() } - .minByOrNull { it.getCenterTile().aerialDistanceTo(unit.currentTile) } ?: return + val ourCitiesWithoutReligion = unit.civInfo.cities.filter { + it.religion.getMajorityReligion() != unit.civInfo.religionManager.religion + } + val city = + if (ourCitiesWithoutReligion.any()) + ourCitiesWithoutReligion.minByOrNull { it.getCenterTile().aerialDistanceTo(unit.getTile()) } + else unit.civInfo.gameInfo.getCities().asSequence() + .filter { it.religion.getMajorityReligion() != unit.civInfo.religionManager.religion } + .filter { it.civInfo.knows(unit.civInfo) && !it.civInfo.isAtWarWith(unit.civInfo) } + .filterNot { it.religion.isProtectedByInquisitor(unit.religion) } + .minByOrNull { it.getCenterTile().aerialDistanceTo(unit.getTile()) } + + if (city == null) return val destination = city.getTiles().asSequence() .filter { unit.movement.canMoveTo(it) || it == unit.getTile() } - .sortedBy { it.aerialDistanceTo(unit.currentTile) } + .sortedBy { it.aerialDistanceTo(unit.getTile()) } .firstOrNull { unit.movement.canReach(it) } ?: return unit.movement.headTowards(destination) - if (unit.civInfo.religionManager.maySpreadReligionNow(unit)) { + if (unit.getTile() in city.getTiles() && unit.civInfo.religionManager.maySpreadReligionNow(unit)) { doReligiousAction(unit, unit.getTile()) } } fun automateInquisitor(unit: MapUnit) { - val cityToConvert = unit.civInfo.cities.asSequence() - .filterNot { it.religion.getMajorityReligion()?.name == null } - .filterNot { it.religion.getMajorityReligion()?.name == unit.religion } - .minByOrNull { it.getCenterTile().aerialDistanceTo(unit.currentTile) } + if (unit.religion != unit.civInfo.religionManager.religion?.name || unit.religion == null) + return unit.disband() // No need to keep a unit we can't use, as it only blocks religion spreads of religions other that its own - val cityToProtect = unit.civInfo.cities.asSequence() - .filter { it.religion.getMajorityReligion()?.name == unit.religion } - .filter { isInquisitorInTheCity(it, unit) } - .maxByOrNull { it.population.population } // cities with most populations will be prioritized by the AI + val holyCity = unit.civInfo.religionManager.getHolyCity() + val cityToConvert = determineBestInquisitorCityToConvert(unit) // Also returns null if the inquisitor can't convert cities + val pressureDeficit = + if (cityToConvert == null) 0 + else cityToConvert.religion.getPressureDeficit(unit.civInfo.religionManager.religion?.name) - var destination: TileInfo? = null + val citiesToProtect = unit.civInfo.cities.asSequence() + .filter { it.religion.getMajorityReligion() == unit.civInfo.religionManager.religion } + // We only look at cities that are not currently protected or are protected by us + .filter { !it.religion.isProtectedByInquisitor() || unit.getTile() in it.getCenterTile().getTilesInDistance(1) } - if (cityToProtect != null) { - destination = cityToProtect.getCenterTile().neighbors.asSequence() - .filter { unit.movement.canMoveTo(it) || it == unit.getTile() } - .sortedBy { it.aerialDistanceTo(unit.currentTile) } - .firstOrNull { unit.movement.canReach(it) } - } - if (destination == null) { - if (cityToConvert == null) return - destination = cityToConvert.getCenterTile().neighbors.asSequence() - .filter { unit.movement.canMoveTo(it) || it == unit.getTile() } - .sortedBy { it.aerialDistanceTo(unit.currentTile) } - .firstOrNull { unit.movement.canReach(it) } + // cities with most populations will be prioritized by the AI + val cityToProtect = citiesToProtect.maxByOrNull { it.population.population } + + var destination: TileInfo? + + destination = when { + cityToConvert != null + && (cityToConvert == holyCity || pressureDeficit > Constants.aiPreferInquisitorOverMissionaryPressureDifference) + && unit.canDoReligiousAction(Constants.removeHeresy) -> { + cityToConvert.getCenterTile() + } + cityToProtect != null && unit.hasUnique(UniqueType.PreventSpreadingReligion) -> { + if (holyCity != null && !holyCity.religion.isProtectedByInquisitor()) + holyCity.getCenterTile() + else cityToProtect.getCenterTile() + } + cityToConvert != null -> cityToConvert.getCenterTile() + else -> null } if (destination == null) return - unit.movement.headTowards(destination) - - - if (cityToConvert != null && unit.currentTile.getCity() == destination.getCity()) { - doReligiousAction(unit, destination) + if (!unit.movement.canReach(destination)) { + destination = destination.neighbors.asSequence() + .filter { unit.movement.canMoveTo(it) || it == unit.getTile() } + .sortedBy { it.aerialDistanceTo(unit.currentTile) } + .firstOrNull { unit.movement.canReach(it) } + ?: return } + unit.movement.headTowards(destination) + + if (cityToConvert != null && unit.getTile().getCity() == destination.getCity()) { + doReligiousAction(unit, destination) + } } - private fun isInquisitorInTheCity(city: CityInfo, unit: MapUnit): Boolean { - if (!city.religion.isProtectedByInquisitor()) - return false + private fun determineBestInquisitorCityToConvert( + unit: MapUnit, + ): CityInfo? { + if (unit.religion != unit.civInfo.religionManager.religion?.name || !unit.canDoReligiousAction(Constants.removeHeresy)) + return null - for (tile in city.getCenterTile().neighbors) - if (unit.currentTile == tile) - return true - return false + val holyCity = unit.civInfo.religionManager.getHolyCity() + if (holyCity != null && holyCity.religion.getMajorityReligion() != unit.civInfo.religionManager.religion!!) + return holyCity + + return unit.civInfo.cities.asSequence() + .filter { it.religion.getMajorityReligion() != null } + .filter { it.religion.getMajorityReligion()!! != unit.civInfo.religionManager.religion } + // Don't go if it takes too long + .filter { it.getCenterTile().aerialDistanceTo(unit.currentTile) <= 20 } + .maxByOrNull { it.religion.getPressureDeficit(unit.civInfo.religionManager.religion?.name) } } - - - fun automateFighter(unit: MapUnit) { val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange()) val enemyAirUnitsInRange = tilesInRange @@ -472,7 +506,12 @@ object SpecificUnitAutomation { for (city in immediatelyReachableCitiesAndCarriers) { if (city.getTilesInDistance(unit.getRange()) - .any { BattleHelper.containsAttackableEnemy(it, MapUnitCombatant(unit)) }) { + .any { + BattleHelper.containsAttackableEnemy( + it, + MapUnitCombatant(unit) + ) + }) { unit.movement.moveToTile(city) return true } @@ -509,10 +548,10 @@ object SpecificUnitAutomation { UnitActions.getEnhanceReligionAction(unit)() } - private fun doReligiousAction(unit: MapUnit, destination: TileInfo){ - val actionList: java.util.ArrayList = ArrayList() - UnitActions.addActionsWithLimitedUses(unit, actionList, destination) - if (actionList.firstOrNull()?.action == null) return - actionList.first().action!!.invoke() + private fun doReligiousAction(unit: MapUnit, destination: TileInfo) { + val religiousActions = ArrayList() + UnitActions.addActionsWithLimitedUses(unit, religiousActions, destination) + if (religiousActions.firstOrNull()?.action == null) return + religiousActions.first().action!!.invoke() } } diff --git a/core/src/com/unciv/logic/automation/UnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt similarity index 98% rename from core/src/com/unciv/logic/automation/UnitAutomation.kt rename to core/src/com/unciv/logic/automation/unit/UnitAutomation.kt index 21028cea2e..c718792a53 100644 --- a/core/src/com/unciv/logic/automation/UnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt @@ -1,6 +1,8 @@ -package com.unciv.logic.automation +package com.unciv.logic.automation.unit import com.unciv.Constants +import com.unciv.logic.automation.Automation +import com.unciv.logic.automation.civilization.NextTurnAutomation import com.unciv.logic.battle.* import com.unciv.logic.city.CityInfo import com.unciv.logic.civilization.ReligionState @@ -160,18 +162,20 @@ object UnitAutomation { //todo this now supports "Great General"-like mod units not combining 'aura' and citadel // abilities, but not additional capabilities if automation finds no use for those two - if (unit.hasStrengthBonusInRadiusUnique && SpecificUnitAutomation.automateGreatGeneral(unit)) + if (unit.hasStrengthBonusInRadiusUnique && SpecificUnitAutomation.automateGreatGeneral( + unit + ) + ) return if (unit.hasCitadelPlacementUnique && SpecificUnitAutomation.automateCitadelPlacer(unit)) return if (unit.hasCitadelPlacementUnique || unit.hasStrengthBonusInRadiusUnique) return SpecificUnitAutomation.automateGreatGeneralFallback(unit) - if (unit.getMatchingUniques(UniqueType.CanActionSeveralTimes).any{ it.params[0] == "Spread Religion" } - && unit.civInfo.religionManager.maySpreadReligionAtAll(unit)) + if (unit.civInfo.religionManager.maySpreadReligionAtAll(unit)) return SpecificUnitAutomation.automateMissionary(unit) - if (unit.hasUnique(UniqueType.PreventSpreadingReligion)) + if (unit.hasUnique(UniqueType.PreventSpreadingReligion) || unit.canDoReligiousAction(Constants.removeHeresy)) return SpecificUnitAutomation.automateInquisitor(unit) if (unit.hasUnique(UniqueType.ConstructImprovementConsumingUnit) @@ -365,9 +369,9 @@ object UnitAutomation { unit.getMaxMovement() * CLOSE_ENEMY_TURNS_AWAY_LIMIT ) var closeEnemies = BattleHelper.getAttackableEnemies( - unit, - unitDistanceToTiles, - tilesToCheck = unit.getTile().getTilesInDistance(CLOSE_ENEMY_TILES_AWAY_LIMIT).toList() + unit, + unitDistanceToTiles, + tilesToCheck = unit.getTile().getTilesInDistance(CLOSE_ENEMY_TILES_AWAY_LIMIT).toList() ).filter { // Ignore units that would 1-shot you if you attacked. Account for taking terrain damage after the fact. BattleDamage.calculateDamageToAttacker( diff --git a/core/src/com/unciv/logic/automation/WorkerAutomation.kt b/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt similarity index 98% rename from core/src/com/unciv/logic/automation/WorkerAutomation.kt rename to core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt index 15a4157acb..3f31e544db 100644 --- a/core/src/com/unciv/logic/automation/WorkerAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt @@ -1,10 +1,13 @@ -package com.unciv.logic.automation +package com.unciv.logic.automation.unit import com.badlogic.gdx.math.Vector2 import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.HexMath -import com.unciv.logic.automation.UnitAutomation.wander +import com.unciv.logic.automation.Automation +import com.unciv.logic.automation.civilization.NextTurnAutomation +import com.unciv.logic.automation.ThreatLevel +import com.unciv.logic.automation.unit.UnitAutomation.wander import com.unciv.logic.city.CityInfo import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.BFS @@ -452,7 +455,9 @@ class WorkerAutomation( ThreatLevel.VeryHigh -> 20 } } - val enemyCivsIsCloseEnough = enemyCivs.filter { NextTurnAutomation.getMinDistanceBetweenCities(civInfo, it) <= threatMapping(it) } + val enemyCivsIsCloseEnough = enemyCivs.filter { NextTurnAutomation.getMinDistanceBetweenCities( + civInfo, + it) <= threatMapping(it) } // no threat, let's not build fort if (enemyCivsIsCloseEnough.isEmpty()) return false diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index af2aec9a33..f2ce55dea5 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -3,7 +3,7 @@ package com.unciv.logic.battle import com.badlogic.gdx.math.Vector2 import com.unciv.Constants import com.unciv.UncivGame -import com.unciv.logic.automation.NextTurnAutomation +import com.unciv.logic.automation.civilization.NextTurnAutomation import com.unciv.logic.city.CityInfo import com.unciv.logic.civilization.AlertType import com.unciv.logic.civilization.CivilizationInfo diff --git a/core/src/com/unciv/logic/battle/GreatGeneralImplementation.kt b/core/src/com/unciv/logic/battle/GreatGeneralImplementation.kt index a1e7397ced..0b5d448d31 100644 --- a/core/src/com/unciv/logic/battle/GreatGeneralImplementation.kt +++ b/core/src/com/unciv/logic/battle/GreatGeneralImplementation.kt @@ -4,7 +4,7 @@ import com.unciv.logic.map.MapUnit import com.unciv.logic.map.TileInfo import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueType -import com.unciv.logic.automation.SpecificUnitAutomation // for Kdoc +import com.unciv.logic.automation.unit.SpecificUnitAutomation // for Kdoc object GreatGeneralImplementation { @@ -79,7 +79,7 @@ object GreatGeneralImplementation { && !tile.isCityCenter() } - // rank tiles and find best + // rank tiles and find best val unitBonusRadius = generalBonusData.maxOfOrNull { it.radius } ?: return null return militaryUnitTilesInDistance diff --git a/core/src/com/unciv/logic/city/CityConstructions.kt b/core/src/com/unciv/logic/city/CityConstructions.kt index e054850645..89cd56b215 100644 --- a/core/src/com/unciv/logic/city/CityConstructions.kt +++ b/core/src/com/unciv/logic/city/CityConstructions.kt @@ -3,7 +3,7 @@ package com.unciv.logic.city import com.unciv.UncivGame import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.automation.Automation -import com.unciv.logic.automation.ConstructionAutomation +import com.unciv.logic.automation.city.ConstructionAutomation import com.unciv.logic.civilization.AlertType import com.unciv.logic.civilization.NotificationIcon import com.unciv.logic.civilization.PopupAlert @@ -496,7 +496,7 @@ class CityConstructions : IsPartOfGameInfoSerialization { } /** - * Purchase a construction for gold + * Purchase a construction for gold (or another stat) * called from NextTurnAutomation and the City UI * Build / place the new item, deduct cost, and maintain queue. * diff --git a/core/src/com/unciv/logic/city/CityReligion.kt b/core/src/com/unciv/logic/city/CityReligion.kt index 1855f8bdc3..79e361960b 100644 --- a/core/src/com/unciv/logic/city/CityReligion.kt +++ b/core/src/com/unciv/logic/city/CityReligion.kt @@ -280,10 +280,17 @@ class CityInfoReligionManager : IsPartOfGameInfoSerialization { return addedPressure } - fun isProtectedByInquisitor(): Boolean { - for (tile in cityInfo.getCenterTile().getTilesInDistance(1)) - if (tile.civilianUnit?.hasUnique(UniqueType.PreventSpreadingReligion) == true) - return true + fun isProtectedByInquisitor(fromReligion: String? = null): Boolean { + for (tile in cityInfo.getCenterTile().getTilesInDistance(1)) { + for (unit in listOf(tile.civilianUnit, tile.militaryUnit)) { + if (unit?.religion != null + && (fromReligion == null || unit.religion != fromReligion) + && unit.hasUnique(UniqueType.PreventSpreadingReligion) + ) { + return true + } + } + } return false } @@ -305,4 +312,8 @@ class CityInfoReligionManager : IsPartOfGameInfoSerialization { return pressure.toInt() } + + fun getPressureDeficit(otherReligion: String?): Int { + return (getPressures()[getMajorityReligionName()] ?: 0) - (getPressures()[otherReligion] ?: 0) + } } diff --git a/core/src/com/unciv/logic/city/IConstruction.kt b/core/src/com/unciv/logic/city/IConstruction.kt index e077461bf3..2af333ab14 100644 --- a/core/src/com/unciv/logic/city/IConstruction.kt +++ b/core/src/com/unciv/logic/city/IConstruction.kt @@ -28,17 +28,18 @@ interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques { fun getRejectionReasons(cityConstructions: CityConstructions): RejectionReasons fun postBuildEvent(cityConstructions: CityConstructions, boughtWith: Stat? = null): Boolean // Yes I'm hilarious. + /** Only checks if it has the unique to be bought with this stat, not whether it is purchasable at all */ fun canBePurchasedWithStat(cityInfo: CityInfo?, stat: Stat): Boolean { if (stat == Stat.Production || stat == Stat.Happiness) return false if (hasUnique(UniqueType.CannotBePurchased)) return false if (stat == Stat.Gold) return !hasUnique(UniqueType.Unbuildable) // Can be purchased with [Stat] [cityFilter] - if (getMatchingUniques(UniqueType.CanBePurchasedWithStat) - .any { cityInfo != null && it.params[0] == stat.name && cityInfo.matchesFilter(it.params[1]) } + if (cityInfo != null && getMatchingUniques(UniqueType.CanBePurchasedWithStat) + .any { it.params[0] == stat.name && cityInfo.matchesFilter(it.params[1]) } ) return true // Can be purchased for [amount] [Stat] [cityFilter] - if (getMatchingUniques(UniqueType.CanBePurchasedForAmountStat) - .any { cityInfo != null && it.params[1] == stat.name && cityInfo.matchesFilter(it.params[2]) } + if (cityInfo != null && getMatchingUniques(UniqueType.CanBePurchasedForAmountStat) + .any { it.params[1] == stat.name && cityInfo.matchesFilter(it.params[2]) } ) return true return false } diff --git a/core/src/com/unciv/logic/civilization/CityStateFunctions.kt b/core/src/com/unciv/logic/civilization/CityStateFunctions.kt index aa2a6babc6..ba4ab9e7cb 100644 --- a/core/src/com/unciv/logic/civilization/CityStateFunctions.kt +++ b/core/src/com/unciv/logic/civilization/CityStateFunctions.kt @@ -1,7 +1,7 @@ package com.unciv.logic.civilization import com.unciv.Constants -import com.unciv.logic.automation.NextTurnAutomation +import com.unciv.logic.automation.civilization.NextTurnAutomation import com.unciv.logic.civilization.diplomacy.* import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.tile.ResourceSupplyList diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index 1c3997ca2d..e24801663f 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -7,8 +7,8 @@ import com.unciv.json.HashMapVector2 import com.unciv.logic.GameInfo import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.UncivShowableException -import com.unciv.logic.automation.NextTurnAutomation -import com.unciv.logic.automation.WorkerAutomation +import com.unciv.logic.automation.civilization.NextTurnAutomation +import com.unciv.logic.automation.unit.WorkerAutomation import com.unciv.logic.city.CityInfo import com.unciv.logic.civilization.RuinsManager.RuinsManager import com.unciv.logic.civilization.diplomacy.DiplomacyFlags diff --git a/core/src/com/unciv/logic/civilization/ReligionManager.kt b/core/src/com/unciv/logic/civilization/ReligionManager.kt index 415b550098..fa43cc2184 100644 --- a/core/src/com/unciv/logic/civilization/ReligionManager.kt +++ b/core/src/com/unciv/logic/civilization/ReligionManager.kt @@ -1,6 +1,8 @@ package com.unciv.logic.civilization +import com.unciv.Constants import com.unciv.logic.IsPartOfGameInfoSerialization +import com.unciv.logic.city.CityInfo import com.unciv.logic.map.MapUnit import com.unciv.models.Counter import com.unciv.models.Religion @@ -150,6 +152,7 @@ class ReligionManager : IsPartOfGameInfoSerialization { } } + /** Calculates the amount of religions that can still be founded */ fun remainingFoundableReligions(): Int { val foundedReligionsCount = civInfo.gameInfo.civilizations.count { it.religionManager.religion != null && it.religionManager.religionState >= ReligionState.Religion @@ -328,10 +331,10 @@ class ReligionManager : IsPartOfGameInfoSerialization { fun maySpreadReligionAtAll(missionary: MapUnit): Boolean { if (!civInfo.gameInfo.isReligionEnabled()) return false // No religion, no spreading - if (religion == null) return false // need a religion + if (religion == null) return false // Need a religion if (religionState < ReligionState.Religion) return false // First found an actual religion if (!civInfo.isMajorCiv()) return false // Only major civs - + if (!missionary.canDoReligiousAction(Constants.spreadReligion)) return false return true } @@ -340,7 +343,7 @@ class ReligionManager : IsPartOfGameInfoSerialization { if (missionary.getTile().getOwner() == null) return false if (missionary.currentTile.owningCity?.religion?.getMajorityReligion()?.name == missionary.religion) return false - if (missionary.getTile().getCity()!!.religion.isProtectedByInquisitor()) return false + if (missionary.getTile().getCity()!!.religion.isProtectedByInquisitor(religion!!.name)) return false return true } @@ -356,6 +359,11 @@ class ReligionManager : IsPartOfGameInfoSerialization { .filter { it.matchesFilter(cityFilter, civInfo) } .sumOf { it.religion.getFollowersOf(religion!!.name)!! } } + + fun getHolyCity(): CityInfo? { + if (religion == null) return null + return civInfo.gameInfo.getCities().firstOrNull { it.religion.religionThisIsTheHolyCityOf == religion!!.name } + } } enum class ReligionState : IsPartOfGameInfoSerialization { diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index 2460032929..fc75b64b1d 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -4,8 +4,8 @@ import com.badlogic.gdx.math.Vector2 import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.IsPartOfGameInfoSerialization -import com.unciv.logic.automation.UnitAutomation -import com.unciv.logic.automation.WorkerAutomation +import com.unciv.logic.automation.unit.UnitAutomation +import com.unciv.logic.automation.unit.WorkerAutomation import com.unciv.logic.battle.Battle import com.unciv.logic.battle.MapUnitCombatant import com.unciv.logic.city.CityInfo @@ -259,8 +259,8 @@ class MapUnit : IsPartOfGameInfoSerialization { fun getTile(): TileInfo = currentTile - // This SHOULD NOT be a HashSet, because if it is, then promotions with the same text (e.g. barrage I, barrage II) - // will not get counted twice! + // This SHOULD NOT be a HashSet, because if it is, then e.g. promotions with the same uniques + // (e.g. barrage I, barrage II) will not get counted twice! @Transient private var tempUniques = ArrayList() diff --git a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt index 09c09666f0..342ed52dc9 100644 --- a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt +++ b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt @@ -274,7 +274,7 @@ class UnitMovementAlgorithms(val unit: MapUnit) { // If the tile is far away, we need to build a path how to get there, and then take the first step if (!distanceToTiles.containsKey(finalDestination)) return getShortestPath(finalDestination).firstOrNull() - ?: throw UnreachableDestinationException("$unit ${unit.currentTile.position} cannot reach $finalDestination") + ?: throw UnreachableDestinationException("$unit ${unit.currentTile} cannot reach $finalDestination") // we should be able to get there this turn if (canMoveTo(finalDestination)) diff --git a/core/src/com/unciv/models/Religion.kt b/core/src/com/unciv/models/Religion.kt index d0f6ee9e4a..a48b9b87ae 100644 --- a/core/src/com/unciv/models/Religion.kt +++ b/core/src/com/unciv/models/Religion.kt @@ -4,7 +4,9 @@ import com.unciv.logic.GameInfo import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.models.ruleset.Belief import com.unciv.models.ruleset.BeliefType +import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.stats.INamed +import com.unciv.models.stats.Stat /** Data object for Religions */ class Religion() : INamed, IsPartOfGameInfoSerialization { @@ -19,6 +21,11 @@ class Religion() : INamed, IsPartOfGameInfoSerialization { @Transient lateinit var gameInfo: GameInfo + @delegate:Transient + val buildingsPurchasableByBeliefs by lazy { + unlockedBuildingsPurchasable() + } + constructor(name: String, gameInfo: GameInfo, foundingCivName: String) : this() { this.name = name this.foundingCivName = foundingCivName @@ -94,4 +101,12 @@ class Religion() : INamed, IsPartOfGameInfoSerialization { fun isEnhancedReligion() = getBeliefs(BeliefType.Enhancer).any() fun getFounder() = gameInfo.civilizations.first { it.civName == foundingCivName } + + private fun unlockedBuildingsPurchasable(): List { + return getAllBeliefsOrdered().flatMap { belief -> + belief.getMatchingUniques(UniqueType.BuyBuildingsWithStat).map { it.params[0] } + + belief.getMatchingUniques(UniqueType.BuyBuildingsForAmountStat).map { it.params[0] } + + belief.getMatchingUniques(UniqueType.BuyBuildingsIncreasingCost).map { it.params[0] } + }.toList() + } } diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt index 294d6ec855..7dd6b3f49c 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt @@ -486,8 +486,8 @@ enum class UniqueParameterType( }, /** For untyped "Can [] [] times" unique */ - Action("action", Constants.spreadReligionAbilityCount, "An action that a unit can perform. Currently, there are only two actions part of this: 'Spread Religion' and 'Remove Foreign religions from your own cities'", "Religious Action Filters") { - private val knownValues = setOf(Constants.spreadReligionAbilityCount, Constants.removeHeresyAbilityCount) + Action("action", Constants.spreadReligion, "An action that a unit can perform. Currently, there are only two actions part of this: 'Spread Religion' and 'Remove Foreign religions from your own cities'", "Religious Action Filters") { + private val knownValues = setOf(Constants.spreadReligion, Constants.removeHeresy) 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 32a4db0bd4..977d24a303 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -672,6 +672,7 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags: OneTimeGainPantheon("Gain enough Faith for a Pantheon", UniqueTarget.Ruins), OneTimeGainProphet("Gain enough Faith for [amount]% of a Great Prophet", UniqueTarget.Ruins), // todo: The "up to [All]" used in vanilla json is not nice to read. Split? + // Or just reword it without the 'up to', so it reads "Reveal [amount/'all'] [tileFilter] tiles within [amount] tiles" OneTimeRevealSpecificMapTiles("Reveal up to [amount/'all'] [tileFilter] within a [amount] tile radius", UniqueTarget.Ruins), OneTimeRevealCrudeMap("From a randomly chosen tile [amount] tiles away from the ruins, reveal tiles up to [amount] tiles away with [amount]% chance", UniqueTarget.Ruins), OneTimeTriggerVoting("Triggers voting for the Diplomatic Victory", UniqueTarget.Triggerable), // used in Building diff --git a/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt b/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt index be97f9805c..7e65fa282d 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt @@ -17,8 +17,8 @@ import com.badlogic.gdx.scenes.scene2d.utils.ClickListener import com.badlogic.gdx.utils.Align import com.unciv.Constants import com.unciv.UncivGame -import com.unciv.logic.automation.BattleHelper -import com.unciv.logic.automation.UnitAutomation +import com.unciv.logic.automation.unit.BattleHelper +import com.unciv.logic.automation.unit.UnitAutomation import com.unciv.logic.battle.Battle import com.unciv.logic.battle.MapUnitCombatant import com.unciv.logic.city.CityInfo diff --git a/core/src/com/unciv/ui/worldscreen/bottombar/BattleTable.kt b/core/src/com/unciv/ui/worldscreen/bottombar/BattleTable.kt index 0d177da568..6de83d015d 100644 --- a/core/src/com/unciv/ui/worldscreen/bottombar/BattleTable.kt +++ b/core/src/com/unciv/ui/worldscreen/bottombar/BattleTable.kt @@ -5,8 +5,8 @@ import com.badlogic.gdx.scenes.scene2d.Touchable import com.badlogic.gdx.scenes.scene2d.ui.Label import com.badlogic.gdx.scenes.scene2d.ui.Table import com.unciv.UncivGame -import com.unciv.logic.automation.BattleHelper -import com.unciv.logic.automation.UnitAutomation +import com.unciv.logic.automation.unit.BattleHelper +import com.unciv.logic.automation.unit.UnitAutomation import com.unciv.logic.battle.Battle import com.unciv.logic.battle.BattleDamage import com.unciv.logic.battle.CityCombatant diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt index 4ab17e4693..3d0d6025e7 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt @@ -2,8 +2,8 @@ package com.unciv.ui.worldscreen.unit import com.unciv.Constants import com.unciv.UncivGame -import com.unciv.logic.automation.UnitAutomation -import com.unciv.logic.automation.WorkerAutomation +import com.unciv.logic.automation.unit.UnitAutomation +import com.unciv.logic.automation.unit.WorkerAutomation import com.unciv.logic.city.CityInfo import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.NotificationIcon @@ -627,8 +627,8 @@ object UnitActions { if (!unit.abilityUsesLeft.containsKey(action)) continue if (unit.abilityUsesLeft[action]!! <= 0) continue when (action) { - Constants.spreadReligionAbilityCount -> addSpreadReligionActions(unit, actionList, city) - Constants.removeHeresyAbilityCount -> addRemoveHeresyActions(unit, actionList, city) + Constants.spreadReligion -> addSpreadReligionActions(unit, actionList, city) + Constants.removeHeresy -> addRemoveHeresyActions(unit, actionList, city) } } } @@ -641,15 +641,7 @@ object UnitActions { } fun addSpreadReligionActions(unit: MapUnit, actionList: ArrayList, city: CityInfo) { - if (!unit.civInfo.gameInfo.isReligionEnabled()) return - val blockedByInquisitor = - city.getCenterTile() - .getTilesInDistance(1) - .flatMap { it.getUnits() } - .any { - it.hasUnique(UniqueType.PreventSpreadingReligion) - && it.religion != unit.religion - } + if (!unit.civInfo.religionManager.maySpreadReligionAtAll(unit)) return actionList += UnitAction(UnitActionType.SpreadReligion, title = "Spread [${unit.getReligionDisplayName()!!}]", action = { @@ -661,8 +653,8 @@ object UnitActions { if (unit.hasUnique(UniqueType.RemoveOtherReligions)) city.religion.removeAllPressuresExceptFor(unit.religion!!) unit.currentMovement = 0f - useActionWithLimitedUses(unit, Constants.spreadReligionAbilityCount) - }.takeIf { unit.currentMovement > 0 && !blockedByInquisitor } + useActionWithLimitedUses(unit, Constants.spreadReligion) + }.takeIf { unit.currentMovement > 0 && unit.civInfo.religionManager.maySpreadReligionNow(unit) } ) } @@ -677,7 +669,7 @@ object UnitActions { action = { city.religion.removeAllPressuresExceptFor(unit.religion!!) unit.currentMovement = 0f - useActionWithLimitedUses(unit, Constants.removeHeresyAbilityCount) + useActionWithLimitedUses(unit, Constants.removeHeresy) }.takeIf { unit.currentMovement > 0f } ) } diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitTable.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitTable.kt index 88212b7084..e28a3fad45 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitTable.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitTable.kt @@ -169,14 +169,14 @@ class UnitTable(val worldScreen: WorldScreen) : Table(){ unitDescriptionTable.add(unit.promotions.XP.toString() + "/" + unit.promotions.xpForNextPromotion()) } - if (unit.canDoReligiousAction(Constants.spreadReligionAbilityCount)) { + if (unit.canDoReligiousAction(Constants.spreadReligion)) { unitDescriptionTable.add(ImageGetter.getStatIcon("Faith")).size(20f) - unitDescriptionTable.add(unit.getActionString(Constants.spreadReligionAbilityCount)) + unitDescriptionTable.add(unit.getActionString(Constants.spreadReligion)) } - if (unit.canDoReligiousAction(Constants.removeHeresyAbilityCount)) { + if (unit.canDoReligiousAction(Constants.removeHeresy)) { unitDescriptionTable.add(ImageGetter.getImage("OtherIcons/Remove Heresy")).size(20f) - unitDescriptionTable.add(unit.getActionString(Constants.removeHeresyAbilityCount)) + unitDescriptionTable.add(unit.getActionString(Constants.removeHeresy)) } if (unit.baseUnit.religiousStrength > 0) {