From 34cb48aa318b7dc4c3e36c82194b0bb03c307957 Mon Sep 17 00:00:00 2001 From: Yair Morgenstern Date: Fri, 17 Sep 2021 09:35:01 +0300 Subject: [PATCH] Unique enum compliance detection (#5226) * Added basic functionality for uniques enum * Added unique type to Unique class for faster enum comparisons * And Elvis operator for unknown parameter type * Resolved #5162 - AI much less motivated to attack city-states * Whoops, wrong branch * New unique checks, with enum. * Fixed Trog's comments --- core/src/com/unciv/models/ruleset/Ruleset.kt | 13 + core/src/com/unciv/models/ruleset/Unique.kt | 555 ++---------------- .../models/ruleset/UniqueTriggerActivation.kt | 485 +++++++++++++++ 3 files changed, 560 insertions(+), 493 deletions(-) create mode 100644 core/src/com/unciv/models/ruleset/UniqueTriggerActivation.kt diff --git a/core/src/com/unciv/models/ruleset/Ruleset.kt b/core/src/com/unciv/models/ruleset/Ruleset.kt index 96d6b2570d..00c825b105 100644 --- a/core/src/com/unciv/models/ruleset/Ruleset.kt +++ b/core/src/com/unciv/models/ruleset/Ruleset.kt @@ -323,6 +323,19 @@ class Ruleset { for (building in buildings.values) { if (building.requiredTech == null && building.cost == 0 && !building.uniques.contains("Unbuildable")) lines += "${building.name} is buildable and therefore must either have an explicit cost or reference an existing tech!" + + for (unique in building.uniqueObjects) { + if (unique.type == null) continue + val complianceErrors = unique.type.getComplianceErrors(unique, this) + for (complianceError in complianceErrors) { + // When not checking the entire ruleset, we can only really detect ruleset-invariant errors + if (complianceError.errorSeverity == UniqueType.UniqueComplianceErrorSeverity.RulesetInvariant) + lines += "${building.name}'s unique \"${unique.text}\" contains parameter ${complianceError.parameterName}," + + " which does not fit parameter type" + + " ${complianceError.acceptableParameterTypes.joinToString(" or ") { it.parameterName }} !" + } + } + } for (nation in nations.values) { diff --git a/core/src/com/unciv/models/ruleset/Unique.kt b/core/src/com/unciv/models/ruleset/Unique.kt index 4b1b484f36..36fc206469 100644 --- a/core/src/com/unciv/models/ruleset/Unique.kt +++ b/core/src/com/unciv/models/ruleset/Unique.kt @@ -1,26 +1,35 @@ package com.unciv.models.ruleset -import com.badlogic.gdx.math.Vector2 -import com.unciv.logic.city.CityInfo -import com.unciv.logic.civilization.* -import com.unciv.logic.map.MapUnit -import com.unciv.logic.map.TileInfo -import com.unciv.models.stats.Stat import com.unciv.models.stats.Stats -import com.unciv.models.translations.fillPlaceholders import com.unciv.models.translations.getPlaceholderParameters import com.unciv.models.translations.getPlaceholderText -import com.unciv.models.translations.hasPlaceholderParameters -import com.unciv.ui.worldscreen.unit.UnitActions -import kotlin.random.Random // parameterName values should be compliant with autogenerated values in TranslationFileWriter.generateStringsFromJSONs // Eventually we'll merge the translation generation to take this as the source of that -enum class UniqueParameterType(val parameterName:String, val complianceCheck:(String, Ruleset)->Boolean) { - Number("amount", { s, r -> s.toIntOrNull() != null }), - UnitFilter("unitType", { s, r -> r.unitTypes.containsKey(s) || unitTypeStrings.contains(s) }), - Unknown("",{s,r -> true}); +enum class UniqueParameterType(val parameterName:String) { + Number("amount") { + override fun getErrorType(parameterText: String, ruleset: Ruleset): + UniqueType.UniqueComplianceErrorSeverity? { + return if (parameterText.toIntOrNull() != null) UniqueType.UniqueComplianceErrorSeverity.RulesetInvariant + else null + } + }, + UnitFilter("unitType") { + override fun getErrorType(parameterText: String, ruleset: Ruleset): + UniqueType.UniqueComplianceErrorSeverity? { + if(ruleset.unitTypes.containsKey(parameterText) || unitTypeStrings.contains(parameterText)) return null + return UniqueType.UniqueComplianceErrorSeverity.RulesetSpecific + } + }, + Unknown("param") { + override fun getErrorType(parameterText: String, ruleset: Ruleset): + UniqueType.UniqueComplianceErrorSeverity? { + return null + } + }; + + abstract fun getErrorType(parameterText:String, ruleset: Ruleset): UniqueType.UniqueComplianceErrorSeverity? companion object { val unitTypeStrings = hashSetOf( @@ -42,6 +51,13 @@ enum class UniqueParameterType(val parameterName:String, val complianceCheck:(St } } +class UniqueComplianceError( + val parameterName: String, + val acceptableParameterTypes: List, + val errorSeverity: UniqueType.UniqueComplianceErrorSeverity +) + + enum class UniqueType(val text:String) { ConsumesResources("Consumes [amount] [resource]"); @@ -61,11 +77,35 @@ enum class UniqueType(val text:String) { val placeholderText = text.getPlaceholderText() - fun checkCompliance(unique: Unique, ruleset: Ruleset): Boolean { - for ((index, param) in unique.params.withIndex()) - if (parameterTypeMap[index].none { it.complianceCheck(param, ruleset) }) - return false - return true + /** Ordinal determines severity - ordered from most severe at 0 */ + enum class UniqueComplianceErrorSeverity { + + /** This is a problem like "numbers don't parse", "stat isn't stat", "city filter not applicable" */ + RulesetInvariant, + + /** This is a problem like "unit/resource/tech name doesn't exist in ruleset" - definite bug */ + RulesetSpecific, + + /** This is for filters that can also potentially accept free text, like UnitFilter and TileFilter */ + WarningOnly + } + + /** Maps uncompliant parameters to their required types */ + fun getComplianceErrors( + unique: Unique, + ruleset: Ruleset + ): List { + val errorList = ArrayList() + for ((index, param) in unique.params.withIndex()) { + val acceptableParamTypes = parameterTypeMap[index] + val errorTypesForAcceptableParameters = + acceptableParamTypes.map { it.getErrorType(param, ruleset) } + if (errorTypesForAcceptableParameters.any { it == null }) continue // This matches one of the types! + val leastSevereWarning = + errorTypesForAcceptableParameters.maxByOrNull { it!!.ordinal }!! + errorList += UniqueComplianceError(param, acceptableParamTypes, leastSevereWarning) + } + return errorList } } @@ -86,8 +126,10 @@ class Unique(val text:String) { fun isOfType(uniqueType: UniqueType) = uniqueType == type + + /** We can't save compliance errors in the unique, since it's ruleset-dependant */ fun matches(uniqueType: UniqueType, ruleset: Ruleset) = isOfType(uniqueType) - && uniqueType.checkCompliance(this, ruleset) + && uniqueType.getComplianceErrors(this, ruleset).isEmpty() } class UniqueMap:HashMap>() { @@ -103,477 +145,4 @@ class UniqueMap:HashMap>() { } fun getAllUniques() = this.asSequence().flatMap { it.value.asSequence() } -} - -// Buildings, techs, policies, ancient ruins and promotions can have 'triggered' effects -object UniqueTriggerActivation { - /** @return boolean whether an action was successfully preformed */ - fun triggerCivwideUnique( - unique: Unique, - civInfo: CivilizationInfo, - cityInfo: CityInfo? = null, - tile: TileInfo? = null, - notification: String? = null - ): Boolean { - val chosenCity = - if (cityInfo != null) cityInfo - else civInfo.cities.firstOrNull { it.isCapital() } - val tileBasedRandom = - if (tile != null) Random(tile.position.toString().hashCode()) - else Random(-550) // Very random indeed - when (unique.placeholderText) { - "Free [] appears" -> { - val unitName = unique.params[0] - val unit = civInfo.gameInfo.ruleSet.units[unitName] - if (chosenCity == null || unit == null || (unit.uniques.contains("Founds a new city") && civInfo.isOneCityChallenger())) - return false - - val placedUnit = civInfo.addUnit(unitName, chosenCity) - if (notification != null && placedUnit != null) { - civInfo.addNotification( - notification, - placedUnit.getTile().position, - placedUnit.name - ) - } - return true - } - "[] free [] units appear" -> { - val unitName = unique.params[1] - val unit = civInfo.gameInfo.ruleSet.units[unitName] - if (chosenCity == null || unit == null || (unit.uniques.contains("Founds a new city") && civInfo.isOneCityChallenger())) - return false - - val tilesUnitsWerePlacedOn: MutableList = mutableListOf() - for (i in 1..unique.params[0].toInt()) { - val placedUnit = civInfo.addUnit(unitName, chosenCity) - if (placedUnit != null) - tilesUnitsWerePlacedOn.add(placedUnit.getTile().position) - } - if (notification != null && tilesUnitsWerePlacedOn.isNotEmpty()) { - civInfo.addNotification( - notification, - LocationAction(tilesUnitsWerePlacedOn), - civInfo.getEquivalentUnit(unit).name - ) - } - return true - } - // Differs from "Free [] appears" in that it spawns near the ruins instead of in a city - "Free [] found in the ruins" -> { - val unit = civInfo.getEquivalentUnit(unique.params[0]) - val placingTile = - tile ?: civInfo.cities.random().getCenterTile() - - val placedUnit = civInfo.placeUnitNearTile(placingTile.position, unit.name) - if (notification != null && placedUnit != null) { - val notificationText = - if (notification.hasPlaceholderParameters()) - notification.fillPlaceholders(unique.params[0]) - else notification - civInfo.addNotification( - notificationText, - placedUnit.getTile().position, - placedUnit.name - ) - } - - return placedUnit != null - } - - // spectators get all techs at start of game, and if (in a mod) a tech gives a free policy, the game gets stuck on the policy picker screen - "Free Social Policy" -> { - if (civInfo.isSpectator()) return false - civInfo.policies.freePolicies++ - if (notification != null) { - civInfo.addNotification(notification, NotificationIcon.Culture) - } - return true - } - "[] Free Social Policies" -> { - if (civInfo.isSpectator()) return false - civInfo.policies.freePolicies += unique.params[0].toInt() - if (notification != null) { - civInfo.addNotification(notification, NotificationIcon.Culture) - } - return true - } - "Empire enters golden age" -> { - civInfo.goldenAges.enterGoldenAge() - if (notification != null) { - civInfo.addNotification(notification, NotificationIcon.Happiness) - } - return true - } - "Free Great Person" -> { - if (civInfo.isSpectator()) return false - if (civInfo.isPlayerCivilization()) { - civInfo.greatPeople.freeGreatPeople++ - if (notification != null) - civInfo.addNotification(notification) // Anyone an idea for a good icon? - return true - } else { - val greatPeople = civInfo.getGreatPeople() - if (greatPeople.isEmpty()) return false - var greatPerson = civInfo.getGreatPeople().random() - - val preferredVictoryType = civInfo.victoryType() - if (preferredVictoryType == VictoryType.Cultural) { - val culturalGP = - greatPeople.firstOrNull { it.uniques.contains("Great Person - [Culture]") } - if (culturalGP != null) greatPerson = culturalGP - } - if (preferredVictoryType == VictoryType.Scientific) { - val scientificGP = - greatPeople.firstOrNull { it.uniques.contains("Great Person - [Science]") } - if (scientificGP != null) greatPerson = scientificGP - } - - return civInfo.addUnit(greatPerson.name, chosenCity) != null - } - } - "[] population []" -> { - val citiesWithPopulationChanged: MutableList = mutableListOf() - for (city in civInfo.cities) { - if (city.matchesFilter(unique.params[1])) { - city.population.addPopulation(unique.params[0].toInt()) - citiesWithPopulationChanged.add(city.location) - } - } - if (notification != null && citiesWithPopulationChanged.isNotEmpty()) - civInfo.addNotification( - notification, - LocationAction(citiesWithPopulationChanged), - NotificationIcon.Population - ) - return citiesWithPopulationChanged.isNotEmpty() - } - "[] population in a random city" -> { - if (civInfo.cities.isEmpty()) return false - val randomCity = civInfo.cities.random(tileBasedRandom) - randomCity.population.addPopulation(unique.params[0].toInt()) - if (notification != null) { - val notificationText = - if (notification.hasPlaceholderParameters()) - notification.fillPlaceholders(randomCity.name) - else notification - civInfo.addNotification( - notificationText, - randomCity.location, - NotificationIcon.Population - ) - } - return true - } - - "Free Technology" -> { - if (civInfo.isSpectator()) return false - civInfo.tech.freeTechs += 1 - if (notification != null) { - civInfo.addNotification(notification, NotificationIcon.Science) - } - return true - } - "[] Free Technologies" -> { - if (civInfo.isSpectator()) return false - civInfo.tech.freeTechs += unique.params[0].toInt() - if (notification != null) { - civInfo.addNotification(notification, NotificationIcon.Science) - } - return true - } - "[] free random researchable Tech(s) from the []" -> { - val researchableTechsFromThatEra = civInfo.gameInfo.ruleSet.technologies.values - .filter { - (it.column!!.era == unique.params[1] || unique.params[1] == "any era") - && civInfo.tech.canBeResearched(it.name) - } - if (researchableTechsFromThatEra.isEmpty()) return false - - val techsToResearch = researchableTechsFromThatEra.shuffled(tileBasedRandom) - .take(unique.params[0].toInt()) - for (tech in techsToResearch) - civInfo.tech.addTechnology(tech.name) - - if (notification != null) { - val notificationText = - if (notification.hasPlaceholderParameters()) - notification.fillPlaceholders(*(techsToResearch.map { it.name } - .toTypedArray())) - else notification - civInfo.addNotification(notificationText, NotificationIcon.Science) - } - - return true - } - - "Quantity of strategic resources produced by the empire increased by 100%" -> { - civInfo.updateDetailedCivResources() - if (notification != null) { - civInfo.addNotification( - notification, - NotificationIcon.War - ) // I'm open for better icons - } - return true - } - "+[]% attack strength to all [] Units for [] turns" -> { - civInfo.temporaryUniques.add(Pair(unique, unique.params[2].toInt())) - if (notification != null) { - civInfo.addNotification(notification, NotificationIcon.War) - } - return true - } - - "Reveals the entire map" -> { - if (notification != null) { - civInfo.addNotification(notification, "UnitIcons/Scout") - } - return civInfo.exploredTiles.addAll( - civInfo.gameInfo.tileMap.values.asSequence().map { it.position }) - } - - "[] units gain the [] promotion" -> { - val filter = unique.params[0] - val promotion = unique.params[1] - - val promotedUnitLocations: MutableList = mutableListOf() - for (unit in civInfo.getCivUnits()) { - if (unit.matchesFilter(filter) - && civInfo.gameInfo.ruleSet.unitPromotions.values.any { - it.name == promotion && unit.type.name in it.unitTypes - } - ) { - unit.promotions.addPromotion(promotion, isFree = true) - promotedUnitLocations.add(unit.getTile().position) - } - } - - if (notification != null) { - civInfo.addNotification( - notification, - LocationAction(promotedUnitLocations), - "unitPromotionIcons/${unique.params[1]}" - ) - } - return promotedUnitLocations.isNotEmpty() - } - - "Allied City-States will occasionally gift Great People" -> { - civInfo.addFlag( - CivFlags.CityStateGreatPersonGift.name, - civInfo.turnsForGreatPersonFromCityState() / 2 - ) - if (notification != null) { - civInfo.addNotification(notification, NotificationIcon.CityState) - } - return true - } - // The mechanics for granting great people are wonky, but basically the following happens: - // Based on the game speed, a timer with some amount of turns is set, 40 on regular speed - // Every turn, 1 is subtracted from this timer, as long as you have at least 1 city state ally - // So no, the number of city-state allies does not matter for this. You have a global timer for all of them combined. - // If the timer reaches the amount of city-state allies you have (or 10, whichever is lower), it is reset. - // You will then receive a random great person from a random city-state you are allied to - // The very first time after acquiring this policy, the timer is set to half of its normal value - // This is the basics, and apart from this, there is some randomness in the exact turn count, but I don't know how much - - // There is surprisingly little information findable online about this policy, and the civ 5 source files are - // also quite though to search through, so this might all be incorrect. - // For now this mechanic seems decent enough that this is fine. - - // Note that the way this is implemented now, this unique does NOT stack - // I could parametrize the [Allied], but eh. - - "Gain [] []" -> { - if (Stat.values().none { it.name == unique.params[1] }) return false - val stat = Stat.valueOf(unique.params[1]) - - if (stat !in listOf(Stat.Gold, Stat.Faith, Stat.Science, Stat.Culture) - || unique.params[0].toIntOrNull() == null - ) return false - - civInfo.addStat(stat, unique.params[0].toInt()) - if (notification != null) - civInfo.addNotification(notification, stat.notificationIcon) - return true - } - "Gain []-[] []" -> { - if (Stat.values().none { it.name == unique.params[2] }) return false - val stat = Stat.valueOf(unique.params[2]) - - if (stat !in listOf(Stat.Gold, Stat.Faith, Stat.Science, Stat.Culture) - || unique.params[0].toIntOrNull() == null - || unique.params[1].toIntOrNull() == null - ) return false - - val foundStatAmount = - (tileBasedRandom.nextInt(unique.params[0].toInt(), unique.params[1].toInt()) * - civInfo.gameInfo.gameParameters.gameSpeed.modifier - ).toInt() - - civInfo.addStat( - Stat.valueOf(unique.params[2]), - foundStatAmount - ) - - if (notification != null) { - val notificationText = - if (notification.hasPlaceholderParameters()) { - notification.fillPlaceholders(foundStatAmount.toString()) - } else notification - civInfo.addNotification(notificationText, stat.notificationIcon) - } - - return true - } - "Gain enough Faith for a Pantheon" -> { - if (civInfo.religionManager.religionState != ReligionState.None) return false - val gainedFaith = civInfo.religionManager.faithForPantheon(2) - if (gainedFaith == 0) return false - - civInfo.addStat(Stat.Faith, gainedFaith) - - if (notification != null) { - val notificationText = - if (notification.hasPlaceholderParameters()) - notification.fillPlaceholders(gainedFaith.toString()) - else notification - civInfo.addNotification(notificationText, NotificationIcon.Faith) - } - - return true - } - "Gain enough Faith for []% of a Great Prophet" -> { - if (civInfo.religionManager.getGreatProphetEquivalent() == null) return false - val gainedFaith = - (civInfo.religionManager.faithForNextGreatProphet() * (unique.params[0].toFloat() / 100f)).toInt() - if (gainedFaith == 0) return false - - civInfo.addStat(Stat.Faith, gainedFaith) - - if (notification != null) { - val notificationText = - if (notification.hasPlaceholderParameters()) - notification.fillPlaceholders(gainedFaith.toString()) - else notification - civInfo.addNotification(notificationText, NotificationIcon.Faith) - } - - return true - } - - "Reveal up to [] [] within a [] tile radius" -> { - if (tile == null) return false - val nearbyRevealableTiles = tile - .getTilesInDistance(unique.params[2].toInt()) - .filter { - !civInfo.exploredTiles.contains(it.position) && it.matchesFilter( - unique.params[1] - ) - } - .map { it.position } - if (nearbyRevealableTiles.none()) return false - civInfo.exploredTiles.addAll(nearbyRevealableTiles - .shuffled(tileBasedRandom) - .apply { - if (unique.params[0] != "All") this.take(unique.params[0].toInt()) - } - ) - - if (notification != null) { - civInfo.addNotification( - notification, - LocationAction(nearbyRevealableTiles.toList()) - ) // We really need a barbarian icon - } - - return true - } - "From a randomly chosen tile [] tiles away from the ruins, reveal tiles up to [] tiles away with []% chance" -> { - if (tile == null) return false - val revealCenter = tile.getTilesAtDistance(unique.params[0].toInt()) - .filter { it.position !in civInfo.exploredTiles } - .toList() - .randomOrNull(tileBasedRandom) - if (revealCenter == null) return false - val tilesToReveal = revealCenter - .getTilesInDistance(unique.params[1].toInt()) - .map { it.position } - .filter { tileBasedRandom.nextFloat() < unique.params[2].toFloat() / 100f } - civInfo.exploredTiles.addAll(tilesToReveal) - civInfo.updateViewableTiles() - if (notification != null) - civInfo.addNotification( - notification, - tile.position, - "ImprovementIcons/Ancient ruins" - ) - } - "Triggers voting for the Diplomatic Victory" -> { - for (civ in civInfo.gameInfo.civilizations) - if (!civ.isBarbarian() && !civ.isSpectator()) - civ.addFlag( - CivFlags.TurnsTillNextDiplomaticVote.name, - civInfo.getTurnsBetweenDiplomaticVotings() - ) - if (notification != null) - civInfo.addNotification(notification, NotificationIcon.Diplomacy) - return true - } - - "Provides the cheapest [] building in your first [] cities for free", - "Provides a [] in your first [] cities for free" -> - civInfo.civConstructions.tryAddFreeBuildings() - } - return false - } - - /** @return boolean whether an action was successfully preformed */ - fun triggerUnitwideUnique( - unique: Unique, - unit: MapUnit, - notification: String? = null - ): Boolean { - when (unique.placeholderText) { - "Heal this unit by [] HP" -> { - unit.healBy(unique.params[0].toInt()) - if (notification != null) - unit.civInfo.addNotification(notification, unit.getTile().position) // Do we have a heal icon? - return true - } - "This Unit gains [] XP" -> { - if (!unit.baseUnit.isMilitary()) return false - unit.promotions.XP += unique.params[0].toInt() - if (notification != null) - unit.civInfo.addNotification(notification, unit.getTile().position) - return true - } - "This Unit upgrades for free" -> { - val upgradeAction = UnitActions.getUpgradeAction(unit, true) - ?: return false - upgradeAction.action!!() - if (notification != null) - unit.civInfo.addNotification(notification, unit.getTile().position) - return true - } - "This Unit upgrades for free including special upgrades" -> { - val upgradeAction = UnitActions.getAncientRuinsUpgradeAction(unit) - ?: return false - upgradeAction.action!!() - if (notification != null) - unit.civInfo.addNotification(notification, unit.getTile().position) - return true - } - "This Unit gains the [] promotion" -> { - val promotion = unit.civInfo.gameInfo.ruleSet.unitPromotions.keys.firstOrNull { it == unique.params[0] } - if (promotion == null) return false - unit.promotions.addPromotion(promotion, true) - if (notification != null) - unit.civInfo.addNotification(notification, unit.name) - return true - } - } - return false - } } \ No newline at end of file diff --git a/core/src/com/unciv/models/ruleset/UniqueTriggerActivation.kt b/core/src/com/unciv/models/ruleset/UniqueTriggerActivation.kt new file mode 100644 index 0000000000..32c8532eac --- /dev/null +++ b/core/src/com/unciv/models/ruleset/UniqueTriggerActivation.kt @@ -0,0 +1,485 @@ +package com.unciv.models.ruleset + +import com.badlogic.gdx.math.Vector2 +import com.unciv.logic.city.CityInfo +import com.unciv.logic.civilization.* +import com.unciv.logic.map.MapUnit +import com.unciv.logic.map.TileInfo +import com.unciv.models.stats.Stat +import com.unciv.models.translations.fillPlaceholders +import com.unciv.models.translations.hasPlaceholderParameters +import com.unciv.ui.worldscreen.unit.UnitActions +import kotlin.random.Random + +// Buildings, techs, policies, ancient ruins and promotions can have 'triggered' effects +object UniqueTriggerActivation { + /** @return boolean whether an action was successfully preformed */ + fun triggerCivwideUnique( + unique: Unique, + civInfo: CivilizationInfo, + cityInfo: CityInfo? = null, + tile: TileInfo? = null, + notification: String? = null + ): Boolean { + val chosenCity = + if (cityInfo != null) cityInfo + else civInfo.cities.firstOrNull { it.isCapital() } + val tileBasedRandom = + if (tile != null) Random(tile.position.toString().hashCode()) + else Random(-550) // Very random indeed + when (unique.placeholderText) { + "Free [] appears" -> { + val unitName = unique.params[0] + val unit = civInfo.gameInfo.ruleSet.units[unitName] + if (chosenCity == null || unit == null || (unit.uniques.contains("Founds a new city") && civInfo.isOneCityChallenger())) + return false + + val placedUnit = civInfo.addUnit(unitName, chosenCity) + if (notification != null && placedUnit != null) { + civInfo.addNotification( + notification, + placedUnit.getTile().position, + placedUnit.name + ) + } + return true + } + "[] free [] units appear" -> { + val unitName = unique.params[1] + val unit = civInfo.gameInfo.ruleSet.units[unitName] + if (chosenCity == null || unit == null || (unit.uniques.contains("Founds a new city") && civInfo.isOneCityChallenger())) + return false + + val tilesUnitsWerePlacedOn: MutableList = mutableListOf() + for (i in 1..unique.params[0].toInt()) { + val placedUnit = civInfo.addUnit(unitName, chosenCity) + if (placedUnit != null) + tilesUnitsWerePlacedOn.add(placedUnit.getTile().position) + } + if (notification != null && tilesUnitsWerePlacedOn.isNotEmpty()) { + civInfo.addNotification( + notification, + LocationAction(tilesUnitsWerePlacedOn), + civInfo.getEquivalentUnit(unit).name + ) + } + return true + } + // Differs from "Free [] appears" in that it spawns near the ruins instead of in a city + "Free [] found in the ruins" -> { + val unit = civInfo.getEquivalentUnit(unique.params[0]) + val placingTile = + tile ?: civInfo.cities.random().getCenterTile() + + val placedUnit = civInfo.placeUnitNearTile(placingTile.position, unit.name) + if (notification != null && placedUnit != null) { + val notificationText = + if (notification.hasPlaceholderParameters()) + notification.fillPlaceholders(unique.params[0]) + else notification + civInfo.addNotification( + notificationText, + placedUnit.getTile().position, + placedUnit.name + ) + } + + return placedUnit != null + } + + // spectators get all techs at start of game, and if (in a mod) a tech gives a free policy, the game gets stuck on the policy picker screen + "Free Social Policy" -> { + if (civInfo.isSpectator()) return false + civInfo.policies.freePolicies++ + if (notification != null) { + civInfo.addNotification(notification, NotificationIcon.Culture) + } + return true + } + "[] Free Social Policies" -> { + if (civInfo.isSpectator()) return false + civInfo.policies.freePolicies += unique.params[0].toInt() + if (notification != null) { + civInfo.addNotification(notification, NotificationIcon.Culture) + } + return true + } + "Empire enters golden age" -> { + civInfo.goldenAges.enterGoldenAge() + if (notification != null) { + civInfo.addNotification(notification, NotificationIcon.Happiness) + } + return true + } + "Free Great Person" -> { + if (civInfo.isSpectator()) return false + if (civInfo.isPlayerCivilization()) { + civInfo.greatPeople.freeGreatPeople++ + if (notification != null) + civInfo.addNotification(notification) // Anyone an idea for a good icon? + return true + } else { + val greatPeople = civInfo.getGreatPeople() + if (greatPeople.isEmpty()) return false + var greatPerson = civInfo.getGreatPeople().random() + + val preferredVictoryType = civInfo.victoryType() + if (preferredVictoryType == VictoryType.Cultural) { + val culturalGP = + greatPeople.firstOrNull { it.uniques.contains("Great Person - [Culture]") } + if (culturalGP != null) greatPerson = culturalGP + } + if (preferredVictoryType == VictoryType.Scientific) { + val scientificGP = + greatPeople.firstOrNull { it.uniques.contains("Great Person - [Science]") } + if (scientificGP != null) greatPerson = scientificGP + } + + return civInfo.addUnit(greatPerson.name, chosenCity) != null + } + } + "[] population []" -> { + val citiesWithPopulationChanged: MutableList = mutableListOf() + for (city in civInfo.cities) { + if (city.matchesFilter(unique.params[1])) { + city.population.addPopulation(unique.params[0].toInt()) + citiesWithPopulationChanged.add(city.location) + } + } + if (notification != null && citiesWithPopulationChanged.isNotEmpty()) + civInfo.addNotification( + notification, + LocationAction(citiesWithPopulationChanged), + NotificationIcon.Population + ) + return citiesWithPopulationChanged.isNotEmpty() + } + "[] population in a random city" -> { + if (civInfo.cities.isEmpty()) return false + val randomCity = civInfo.cities.random(tileBasedRandom) + randomCity.population.addPopulation(unique.params[0].toInt()) + if (notification != null) { + val notificationText = + if (notification.hasPlaceholderParameters()) + notification.fillPlaceholders(randomCity.name) + else notification + civInfo.addNotification( + notificationText, + randomCity.location, + NotificationIcon.Population + ) + } + return true + } + + "Free Technology" -> { + if (civInfo.isSpectator()) return false + civInfo.tech.freeTechs += 1 + if (notification != null) { + civInfo.addNotification(notification, NotificationIcon.Science) + } + return true + } + "[] Free Technologies" -> { + if (civInfo.isSpectator()) return false + civInfo.tech.freeTechs += unique.params[0].toInt() + if (notification != null) { + civInfo.addNotification(notification, NotificationIcon.Science) + } + return true + } + "[] free random researchable Tech(s) from the []" -> { + val researchableTechsFromThatEra = civInfo.gameInfo.ruleSet.technologies.values + .filter { + (it.column!!.era == unique.params[1] || unique.params[1] == "any era") + && civInfo.tech.canBeResearched(it.name) + } + if (researchableTechsFromThatEra.isEmpty()) return false + + val techsToResearch = researchableTechsFromThatEra.shuffled(tileBasedRandom) + .take(unique.params[0].toInt()) + for (tech in techsToResearch) + civInfo.tech.addTechnology(tech.name) + + if (notification != null) { + val notificationText = + if (notification.hasPlaceholderParameters()) + notification.fillPlaceholders(*(techsToResearch.map { it.name } + .toTypedArray())) + else notification + civInfo.addNotification(notificationText, NotificationIcon.Science) + } + + return true + } + + "Quantity of strategic resources produced by the empire increased by 100%" -> { + civInfo.updateDetailedCivResources() + if (notification != null) { + civInfo.addNotification( + notification, + NotificationIcon.War + ) // I'm open for better icons + } + return true + } + "+[]% attack strength to all [] Units for [] turns" -> { + civInfo.temporaryUniques.add(Pair(unique, unique.params[2].toInt())) + if (notification != null) { + civInfo.addNotification(notification, NotificationIcon.War) + } + return true + } + + "Reveals the entire map" -> { + if (notification != null) { + civInfo.addNotification(notification, "UnitIcons/Scout") + } + return civInfo.exploredTiles.addAll( + civInfo.gameInfo.tileMap.values.asSequence().map { it.position }) + } + + "[] units gain the [] promotion" -> { + val filter = unique.params[0] + val promotion = unique.params[1] + + val promotedUnitLocations: MutableList = mutableListOf() + for (unit in civInfo.getCivUnits()) { + if (unit.matchesFilter(filter) + && civInfo.gameInfo.ruleSet.unitPromotions.values.any { + it.name == promotion && unit.type.name in it.unitTypes + } + ) { + unit.promotions.addPromotion(promotion, isFree = true) + promotedUnitLocations.add(unit.getTile().position) + } + } + + if (notification != null) { + civInfo.addNotification( + notification, + LocationAction(promotedUnitLocations), + "unitPromotionIcons/${unique.params[1]}" + ) + } + return promotedUnitLocations.isNotEmpty() + } + + "Allied City-States will occasionally gift Great People" -> { + civInfo.addFlag( + CivFlags.CityStateGreatPersonGift.name, + civInfo.turnsForGreatPersonFromCityState() / 2 + ) + if (notification != null) { + civInfo.addNotification(notification, NotificationIcon.CityState) + } + return true + } + // The mechanics for granting great people are wonky, but basically the following happens: + // Based on the game speed, a timer with some amount of turns is set, 40 on regular speed + // Every turn, 1 is subtracted from this timer, as long as you have at least 1 city state ally + // So no, the number of city-state allies does not matter for this. You have a global timer for all of them combined. + // If the timer reaches the amount of city-state allies you have (or 10, whichever is lower), it is reset. + // You will then receive a random great person from a random city-state you are allied to + // The very first time after acquiring this policy, the timer is set to half of its normal value + // This is the basics, and apart from this, there is some randomness in the exact turn count, but I don't know how much + + // There is surprisingly little information findable online about this policy, and the civ 5 source files are + // also quite though to search through, so this might all be incorrect. + // For now this mechanic seems decent enough that this is fine. + + // Note that the way this is implemented now, this unique does NOT stack + // I could parametrize the [Allied], but eh. + + "Gain [] []" -> { + if (Stat.values().none { it.name == unique.params[1] }) return false + val stat = Stat.valueOf(unique.params[1]) + + if (stat !in listOf(Stat.Gold, Stat.Faith, Stat.Science, Stat.Culture) + || unique.params[0].toIntOrNull() == null + ) return false + + civInfo.addStat(stat, unique.params[0].toInt()) + if (notification != null) + civInfo.addNotification(notification, stat.notificationIcon) + return true + } + "Gain []-[] []" -> { + if (Stat.values().none { it.name == unique.params[2] }) return false + val stat = Stat.valueOf(unique.params[2]) + + if (stat !in listOf(Stat.Gold, Stat.Faith, Stat.Science, Stat.Culture) + || unique.params[0].toIntOrNull() == null + || unique.params[1].toIntOrNull() == null + ) return false + + val foundStatAmount = + (tileBasedRandom.nextInt(unique.params[0].toInt(), unique.params[1].toInt()) * + civInfo.gameInfo.gameParameters.gameSpeed.modifier + ).toInt() + + civInfo.addStat( + Stat.valueOf(unique.params[2]), + foundStatAmount + ) + + if (notification != null) { + val notificationText = + if (notification.hasPlaceholderParameters()) { + notification.fillPlaceholders(foundStatAmount.toString()) + } else notification + civInfo.addNotification(notificationText, stat.notificationIcon) + } + + return true + } + "Gain enough Faith for a Pantheon" -> { + if (civInfo.religionManager.religionState != ReligionState.None) return false + val gainedFaith = civInfo.religionManager.faithForPantheon(2) + if (gainedFaith == 0) return false + + civInfo.addStat(Stat.Faith, gainedFaith) + + if (notification != null) { + val notificationText = + if (notification.hasPlaceholderParameters()) + notification.fillPlaceholders(gainedFaith.toString()) + else notification + civInfo.addNotification(notificationText, NotificationIcon.Faith) + } + + return true + } + "Gain enough Faith for []% of a Great Prophet" -> { + if (civInfo.religionManager.getGreatProphetEquivalent() == null) return false + val gainedFaith = + (civInfo.religionManager.faithForNextGreatProphet() * (unique.params[0].toFloat() / 100f)).toInt() + if (gainedFaith == 0) return false + + civInfo.addStat(Stat.Faith, gainedFaith) + + if (notification != null) { + val notificationText = + if (notification.hasPlaceholderParameters()) + notification.fillPlaceholders(gainedFaith.toString()) + else notification + civInfo.addNotification(notificationText, NotificationIcon.Faith) + } + + return true + } + + "Reveal up to [] [] within a [] tile radius" -> { + if (tile == null) return false + val nearbyRevealableTiles = tile + .getTilesInDistance(unique.params[2].toInt()) + .filter { + !civInfo.exploredTiles.contains(it.position) && it.matchesFilter( + unique.params[1] + ) + } + .map { it.position } + if (nearbyRevealableTiles.none()) return false + civInfo.exploredTiles.addAll(nearbyRevealableTiles + .shuffled(tileBasedRandom) + .apply { + if (unique.params[0] != "All") this.take(unique.params[0].toInt()) + } + ) + + if (notification != null) { + civInfo.addNotification( + notification, + LocationAction(nearbyRevealableTiles.toList()) + ) // We really need a barbarian icon + } + + return true + } + "From a randomly chosen tile [] tiles away from the ruins, reveal tiles up to [] tiles away with []% chance" -> { + if (tile == null) return false + val revealCenter = tile.getTilesAtDistance(unique.params[0].toInt()) + .filter { it.position !in civInfo.exploredTiles } + .toList() + .randomOrNull(tileBasedRandom) + if (revealCenter == null) return false + val tilesToReveal = revealCenter + .getTilesInDistance(unique.params[1].toInt()) + .map { it.position } + .filter { tileBasedRandom.nextFloat() < unique.params[2].toFloat() / 100f } + civInfo.exploredTiles.addAll(tilesToReveal) + civInfo.updateViewableTiles() + if (notification != null) + civInfo.addNotification( + notification, + tile.position, + "ImprovementIcons/Ancient ruins" + ) + } + "Triggers voting for the Diplomatic Victory" -> { + for (civ in civInfo.gameInfo.civilizations) + if (!civ.isBarbarian() && !civ.isSpectator()) + civ.addFlag( + CivFlags.TurnsTillNextDiplomaticVote.name, + civInfo.getTurnsBetweenDiplomaticVotings() + ) + if (notification != null) + civInfo.addNotification(notification, NotificationIcon.Diplomacy) + return true + } + + "Provides the cheapest [] building in your first [] cities for free", + "Provides a [] in your first [] cities for free" -> + civInfo.civConstructions.tryAddFreeBuildings() + } + return false + } + + /** @return boolean whether an action was successfully preformed */ + fun triggerUnitwideUnique( + unique: Unique, + unit: MapUnit, + notification: String? = null + ): Boolean { + when (unique.placeholderText) { + "Heal this unit by [] HP" -> { + unit.healBy(unique.params[0].toInt()) + if (notification != null) + unit.civInfo.addNotification(notification, unit.getTile().position) // Do we have a heal icon? + return true + } + "This Unit gains [] XP" -> { + if (!unit.baseUnit.isMilitary()) return false + unit.promotions.XP += unique.params[0].toInt() + if (notification != null) + unit.civInfo.addNotification(notification, unit.getTile().position) + return true + } + "This Unit upgrades for free" -> { + val upgradeAction = UnitActions.getUpgradeAction(unit, true) + ?: return false + upgradeAction.action!!() + if (notification != null) + unit.civInfo.addNotification(notification, unit.getTile().position) + return true + } + "This Unit upgrades for free including special upgrades" -> { + val upgradeAction = UnitActions.getAncientRuinsUpgradeAction(unit) + ?: return false + upgradeAction.action!!() + if (notification != null) + unit.civInfo.addNotification(notification, unit.getTile().position) + return true + } + "This Unit gains the [] promotion" -> { + val promotion = unit.civInfo.gameInfo.ruleSet.unitPromotions.keys.firstOrNull { it == unique.params[0] } + if (promotion == null) return false + unit.promotions.addPromotion(promotion, true) + if (notification != null) + unit.civInfo.addNotification(notification, unit.name) + return true + } + } + return false + } +} \ No newline at end of file