From the indstrial era onwards, things change in religion (#5095)

* Improved redability

* From the industrial era onwards, religion goes into 'second phase'

* Fixed tests

* Fixed formula for buying great prophets starting from the industrial era

* Added `getMatchingUniques`, `hasUnique` to `IHasUniques`, cleaned up some code

* Fix compilation errors
This commit is contained in:
Xander Lenstra
2021-09-08 20:24:26 +02:00
committed by GitHub
parent 65695496f3
commit 3722fab38d
15 changed files with 174 additions and 84 deletions

View File

@ -137,7 +137,11 @@
"Mercantile": ["Provides [3] Happiness", "Provides a unique luxury"],
"Militaristic": ["Provides military units every [17] turns"]
},
"iconRGB": [63, 81, 182]
"iconRGB": [63, 81, 182],
"uniques": ["May not generate great prophet equivalents naturally",
"May buy [Great Prophet] units for [200] [Faith] [in all cities in which the majority religion is a major religion] at an increasing price ([100])",
"Starting in this era disables religion"
]
},
{
"name": "Modern era",
@ -168,7 +172,11 @@
"Mercantile": ["Provides [3] Happiness", "Provides a unique luxury"],
"Militaristic": ["Provides military units every [17] turns"]
},
"iconRGB": [33, 150, 243]
"iconRGB": [33, 150, 243],
"uniques": ["May not generate great prophet equivalents naturally",
"May buy [Great Prophet] units for [200] [Faith] [in all cities in which the majority religion is a major religion] at an increasing price ([100])",
"Starting in this era disables religion"
]
},
{
"name": "Atomic era",
@ -200,7 +208,11 @@
"Mercantile": ["Provides [3] Happiness", "Provides a unique luxury"],
"Militaristic": ["Provides military units every [17] turns"]
},
"iconRGB": [0, 150, 136]
"iconRGB": [0, 150, 136],
"uniques": ["May not generate great prophet equivalents naturally",
"May buy [Great Prophet] units for [200] [Faith] [in all cities in which the majority religion is a major religion] at an increasing price ([100])",
"Starting in this era disables religion"
]
},
{
"name": "Information era",
@ -236,7 +248,11 @@
"Mercantile": ["Provides [3] Happiness", "Provides a unique luxury"],
"Militaristic": ["Provides military units every [17] turns"]
},
"iconRGB": [76, 176, 81]
"iconRGB": [76, 176, 81],
"uniques": ["May not generate great prophet equivalents naturally",
"May buy [Great Prophet] units for [200] [Faith] [in all cities in which the majority religion is a major religion] at an increasing price ([100])",
"Starting in this era disables religion"
]
},
{ // Technically, this Era doesn't exist in the original game.
// But as it is _really_ usefull to have for testing, I'd like to keep it.
@ -271,6 +287,10 @@
"Mercantile": ["Provides [3] Happiness", "Provides a unique luxury"],
"Militaristic": ["Provides military units every [17] turns"]
},
"iconRGB": [76, 176, 81]
"iconRGB": [76, 176, 81],
"uniques": ["May not generate great prophet equivalents naturally",
"May buy [Great Prophet] units for [200] [Faith] [in all cities in which the majority religion is a major religion] at an increasing price ([100])",
"Starting in this era disables religion"
]
}
]

View File

@ -433,7 +433,10 @@ class GameInfo {
}
fun hasReligionEnabled() = gameParameters.religionEnabled || ruleSet.hasReligion() // Temporary function to check whether religion should be used for this game
fun hasReligionEnabled() =
// Temporary function to check whether religion should be used for this game
(gameParameters.religionEnabled || ruleSet.hasReligion())
&& (ruleSet.eras.isEmpty() || !ruleSet.eras[gameParameters.startingEra]!!.hasUnique("Starting in this era disables religion"))
}
// reduced variant only for load preview

View File

@ -43,15 +43,15 @@ class CityInfo {
lateinit var tilesInRange: HashSet<TileInfo>
@Transient
var hasJustBeenConquered =
false // this is so that military units can enter the city, even before we decide what to do with it
// This is so that military units can enter the city, even before we decide what to do with it
var hasJustBeenConquered = false
var location: Vector2 = Vector2.Zero
var id: String = UUID.randomUUID().toString()
var name: String = ""
var foundingCiv = ""
var previousOwner =
"" // This is so that cities in resistance that re recaptured aren't in resistance anymore
// This is so that cities in resistance that are recaptured aren't in resistance anymore
var previousOwner = ""
var turnAcquired = 0
var health = 200
var resistanceCounter = 0
@ -245,8 +245,10 @@ class CityInfo {
fun isCapital(): Boolean = cityConstructions.builtBuildings.contains(capitalCityIndicator())
fun isCoastal(): Boolean = centerTileInfo.isCoastalTile()
fun capitalCityIndicator(): String {
val indicatorBuildings = getRuleset().buildings.values.asSequence()
val indicatorBuildings = getRuleset().buildings.values
.asSequence()
.filter { it.uniques.contains("Indicates the capital city") }
val civSpecificBuilding = indicatorBuildings.firstOrNull { it.uniqueTo == civInfo.civName }
if (civSpecificBuilding != null) return civSpecificBuilding.name
else return indicatorBuildings.first().name
@ -294,8 +296,9 @@ class CityInfo {
val resource = getRuleset().tileResources[unique.params[1]]
if (resource != null) {
cityResources.add(
resource, unique.params[0].toInt()
* civInfo.getResourceModifier(resource), "Tiles"
resource,
unique.params[0].toInt() * civInfo.getResourceModifier(resource),
"Tiles"
)
}
}
@ -596,11 +599,15 @@ class CityInfo {
*/
private fun triggerCitiesSettledNearOtherCiv() {
val citiesWithin6Tiles =
civInfo.gameInfo.civilizations.filter { it.isMajorCiv() && it != civInfo }
civInfo.gameInfo.civilizations
.filter { it.isMajorCiv() && it != civInfo }
.flatMap { it.cities }
.filter { it.getCenterTile().aerialDistanceTo(getCenterTile()) <= 6 }
val civsWithCloseCities = citiesWithin6Tiles.map { it.civInfo }.distinct()
.filter { it.knows(civInfo) && it.exploredTiles.contains(location) }
val civsWithCloseCities =
citiesWithin6Tiles
.map { it.civInfo }
.distinct()
.filter { it.knows(civInfo) && it.exploredTiles.contains(location) }
for (otherCiv in civsWithCloseCities)
otherCiv.getDiplomacyManager(civInfo).setFlag(DiplomacyFlags.SettledCitiesNearUs, 30)
}
@ -627,13 +634,13 @@ class CityInfo {
"in all cities with a garrison" -> getCenterTile().militaryUnit != null
"in all cities in which the majority religion is a major religion" ->
religion.getMajorityReligionName() != null
&& religion.getMajorityReligion()!!.isMajorReligion()
&& religion.getMajorityReligion()!!.isMajorReligion()
"in all cities in which the majority religion is an enhanced religion" ->
religion.getMajorityReligionName() != null
&& religion.getMajorityReligion()!!.isEnhancedReligion()
&& religion.getMajorityReligion()!!.isEnhancedReligion()
"in non-enemy foreign cities" ->
viewingCiv != civInfo
&& !civInfo.isAtWarWith(viewingCiv)
&& !civInfo.isAtWarWith(viewingCiv)
"in foreign cities" -> viewingCiv != civInfo
"in annexed cities" -> foundingCiv != civInfo.civName && !isPuppet
"in holy cities" -> religion.religionThisIsTheHolyCityOf != null
@ -715,9 +722,11 @@ class CityInfo {
if (!tilesList.contains(tile))
cityPositionList.add(tile)
return cityPositionList.asSequence()
.map { it.getOwner()?.civName }.filterNotNull()
.distinct().toList()
return cityPositionList
.asSequence()
.mapNotNull { it.getOwner()?.civName }
.distinct()
.toList()
}
fun getImprovableTiles(): Sequence<TileInfo> = getTiles()

View File

@ -24,13 +24,6 @@ interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques {
fun getRejectionReasons(cityConstructions: CityConstructions): RejectionReasons
fun postBuildEvent(cityConstructions: CityConstructions, boughtWith: Stat? = null): Boolean // Yes I'm hilarious.
fun getMatchingUniques(uniqueTemplate: String): Sequence<Unique> {
return uniqueObjects.asSequence().filter { it.placeholderText == uniqueTemplate }
}
fun hasUnique(uniqueTemplate: String): Boolean {
return uniqueObjects.any { it.placeholderText == uniqueTemplate }
}
fun canBePurchasedWithStat(cityInfo: CityInfo?, stat: Stat): Boolean {
if (stat in listOf(Stat.Production, Stat.Happiness)) return false
if ("Cannot be purchased" in uniques) return false
@ -82,41 +75,46 @@ interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques {
class RejectionReasons(): HashSet<RejectionReason>() {
private val techPolicyEraWonderRequirements = hashSetOf(
RejectionReason.Obsoleted,
RejectionReason.RequiresTech,
RejectionReason.RequiresPolicy,
RejectionReason.MorePolicyBranches,
RejectionReason.RequiresBuildingInSomeCity
)
fun filterTechPolicyEraWonderRequirements(): HashSet<RejectionReason> {
return filterNot { it in techPolicyEraWonderRequirements }.toHashSet()
}
private val reasonsToDefinitivelyRemoveFromQueue = hashSetOf(
RejectionReason.Obsoleted,
RejectionReason.WonderAlreadyBuilt,
RejectionReason.NationalWonderAlreadyBuilt,
RejectionReason.CannotBeBuiltWith,
RejectionReason.ReachedBuildCap
)
fun hasAReasonToBeRemovedFromQueue(): Boolean {
return any { it in reasonsToDefinitivelyRemoveFromQueue }
}
private val orderOfErrorMessages = listOf(
RejectionReason.WonderBeingBuiltElsewhere,
RejectionReason.NationalWonderBeingBuiltElsewhere,
RejectionReason.RequiresBuildingInAllCities,
RejectionReason.RequiresBuildingInThisCity,
RejectionReason.RequiresBuildingInSomeCity,
RejectionReason.PopulationRequirement,
RejectionReason.ConsumesResources,
RejectionReason.CanOnlyBePurchased
)
fun getMostImportantRejectionReason(): String? {
return orderOfErrorMessages.firstOrNull { it in this }?.errorMessage
}
// Used for constant variables in the functions above
companion object {
private val techPolicyEraWonderRequirements = hashSetOf(
RejectionReason.Obsoleted,
RejectionReason.RequiresTech,
RejectionReason.RequiresPolicy,
RejectionReason.MorePolicyBranches,
RejectionReason.RequiresBuildingInSomeCity
)
private val reasonsToDefinitivelyRemoveFromQueue = hashSetOf(
RejectionReason.Obsoleted,
RejectionReason.WonderAlreadyBuilt,
RejectionReason.NationalWonderAlreadyBuilt,
RejectionReason.CannotBeBuiltWith,
RejectionReason.ReachedBuildCap
)
private val orderOfErrorMessages = listOf(
RejectionReason.WonderBeingBuiltElsewhere,
RejectionReason.NationalWonderBeingBuiltElsewhere,
RejectionReason.RequiresBuildingInAllCities,
RejectionReason.RequiresBuildingInThisCity,
RejectionReason.RequiresBuildingInSomeCity,
RejectionReason.PopulationRequirement,
RejectionReason.ConsumesResources,
RejectionReason.CanOnlyBePurchased
)
}
}

View File

@ -293,11 +293,15 @@ class CivilizationInfo {
temporaryUniques
.asSequence()
.filter { it.first.placeholderText == uniqueTemplate }.map { it.first } +
if (religionManager.religion != null)
religionManager.religion!!.getFounderUniques()
.asSequence()
.filter { it.placeholderText == uniqueTemplate }
else sequenceOf()
getEra().getMatchingUniques(uniqueTemplate)
.asSequence() +
(
if (religionManager.religion != null)
religionManager.religion!!.getFounderUniques()
.asSequence()
.filter { it.placeholderText == uniqueTemplate }
else sequenceOf()
)
}
//region Units

View File

@ -27,8 +27,11 @@ class ReligionManager {
// But the other one should still be _somewhere_. So our only option is to have the GameInfo
// contain the master list, and the ReligionManagers retrieve it from there every time the game loads.
var greatProphetsEarned = 0
private set
// Deprecated since 3.16.13
@Deprecated("Replace by adding to `civInfo.boughtConstructionsWithGloballyIncreasingPrice`")
var greatProphetsEarned = 0
private set
//
var religionState = ReligionState.None
private set
@ -47,7 +50,6 @@ class ReligionManager {
clone.shouldChoosePantheonBelief = shouldChoosePantheonBelief
clone.storedFaith = storedFaith
clone.religionState = religionState
clone.greatProphetsEarned = greatProphetsEarned
return clone
}
@ -62,6 +64,13 @@ class ReligionManager {
religion = civInfo.gameInfo.religions.values.firstOrNull {
it.foundingCivName == civInfo.civName
}
// greatProphetsEarned deprecated since 3.16.13, replacement code
if (greatProphetsEarned != 0) {
civInfo.boughtConstructionsWithGloballyIncreasingPrice[getGreatProphetEquivalent()!!] = greatProphetsEarned
greatProphetsEarned = 0
}
//
}
fun startTurn() {
@ -106,6 +115,8 @@ class ReligionManager {
// https://www.reddit.com/r/civ/comments/2m82wu/can_anyone_detail_the_finer_points_of_great/
// Game files (globaldefines.xml)
fun faithForNextGreatProphet(): Int {
val greatProphetsEarned = civInfo.boughtConstructionsWithGloballyIncreasingPrice[getGreatProphetEquivalent()!!] ?: 0
var faithCost =
(200 + 100 * greatProphetsEarned * (greatProphetsEarned + 1) / 2f) *
civInfo.gameInfo.gameParameters.gameSpeed.modifier
@ -120,22 +131,28 @@ class ReligionManager {
if (religion == null || religionState == ReligionState.None) return false // First get a pantheon, then we'll talk about a real religion
if (storedFaith < faithForNextGreatProphet()) return false
if (!civInfo.isMajorCiv()) return false
// In the base game, great prophets shouldn't generate anymore starting from the industrial era
// This is difficult to implement in the current codebase, probably requires an additional variable in eras.json
if (civInfo.hasUnique("May not generate great prophet equivalents naturally")) return false
return true
}
fun getGreatProphetEquivalent(): String? {
return civInfo.gameInfo.ruleSet.units.values.firstOrNull { it.hasUnique("May found a religion") }?.name
}
private fun generateProphet() {
val prophetUnitName = getGreatProphetEquivalent() ?: return // No prophet units in this mod
val prophetSpawnChange = (5f + storedFaith - faithForNextGreatProphet()) / 100f
if (Random(civInfo.gameInfo.turns).nextFloat() < prophetSpawnChange) {
val birthCity =
if (religionState <= ReligionState.Pantheon) civInfo.getCapital()
else civInfo.cities.firstOrNull { it.religion.religionThisIsTheHolyCityOf == religion!!.name }
val prophet = civInfo.addUnit("Great Prophet", birthCity) ?: return
val prophet = civInfo.addUnit(prophetUnitName, birthCity) ?: return
prophet.religion = religion!!.name
storedFaith -= faithForNextGreatProphet()
greatProphetsEarned += 1
civInfo.boughtConstructionsWithGloballyIncreasingPrice[prophetUnitName] =
(civInfo.boughtConstructionsWithGloballyIncreasingPrice[prophetUnitName] ?: 0) + 1
}
}

View File

@ -42,7 +42,9 @@ class RuinsManager {
for (possibleReward in possibleRewards) {
if (civInfo.gameInfo.difficulty in possibleReward.excludedDifficulties) continue
if (Constants.hiddenWithoutReligionUnique in possibleReward.uniques && !civInfo.gameInfo.hasReligionEnabled()) continue
if ("Hidden after generating a Great Prophet" in possibleReward.uniques && civInfo.religionManager.greatProphetsEarned > 0) continue
if ("Hidden after generating a Great Prophet" in possibleReward.uniques
&& civInfo.boughtConstructionsWithGloballyIncreasingPrice[civInfo.religionManager.getGreatProphetEquivalent()] ?: 0 > 0
) continue
if (possibleReward.uniqueObjects.any { unique ->
unique.placeholderText == "Only available after [] turns"
&& unique.params[0].toInt() < civInfo.gameInfo.turns

View File

@ -5,7 +5,7 @@ import com.unciv.logic.civilization.CityStateType
import com.unciv.models.stats.INamed
import com.unciv.ui.utils.colorFromRGB
class Era : INamed {
class Era : INamed, IHasUniques {
override var name: String = ""
var eraNumber: Int = -1
var researchAgreementCost = 300
@ -25,6 +25,8 @@ class Era : INamed {
var friendBonus = HashMap<String, List<String>>()
var allyBonus = HashMap<String, List<String>>()
var iconRGB: List<Int>? = null
override var uniques: ArrayList<String> = arrayListOf()
override val uniqueObjects: List<Unique> by lazy { uniques.map { Unique(it) } }
fun getStartingUnits(): List<String> {
val startingUnits = mutableListOf<String>()

View File

@ -6,4 +6,8 @@ package com.unciv.models.ruleset
interface IHasUniques {
var uniques: ArrayList<String> // Can not be a hashset as that would remove doubles
val uniqueObjects: List<Unique>
fun getMatchingUniques(uniqueTemplate: String) = uniqueObjects.asSequence().filter { it.placeholderText == uniqueTemplate }
fun hasUnique(uniqueTemplate: String) = uniqueObjects.any { it.placeholderText == uniqueTemplate }
}

View File

@ -10,6 +10,7 @@ class RuinReward : INamed, ICivilopediaText, IHasUniques {
override var uniques = ArrayList<String>()
@delegate:Transient // Defense in depth against mad modders
override val uniqueObjects: List<Unique> by lazy { uniques.map { Unique(it) } }
val excludedDifficulties: List<String> = listOf()
val weight: Int = 1
val color: String = "" // For Civilopedia

View File

@ -3,6 +3,7 @@ package com.unciv.models.ruleset.tile
import com.badlogic.gdx.graphics.Color
import com.unciv.Constants
import com.unciv.models.ruleset.Belief
import com.unciv.models.ruleset.IHasUniques
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.Unique
import com.unciv.models.stats.NamedStats
@ -10,7 +11,7 @@ import com.unciv.ui.civilopedia.FormattedLine
import com.unciv.ui.civilopedia.ICivilopediaText
import com.unciv.ui.utils.colorFromRGB
class Terrain : NamedStats(), ICivilopediaText {
class Terrain : NamedStats(), ICivilopediaText, IHasUniques {
lateinit var type: TerrainType
@ -26,8 +27,8 @@ class Terrain : NamedStats(), ICivilopediaText {
val turnsInto: String? = null
/** Uniques (Properties such as Temp/humidity, Fresh water, elevation, rough, defense, Natural Wonder specials) */
val uniques = ArrayList<String>()
val uniqueObjects: List<Unique> by lazy { uniques.map { Unique(it) } }
override var uniques = ArrayList<String>()
override val uniqueObjects: List<Unique> by lazy { uniques.map { Unique(it) } }
/** Natural Wonder weight: probability to be picked */
var weight = 10

View File

@ -70,7 +70,6 @@ class TileImprovement : NamedStats(), ICivilopediaText, IHasUniques {
return lines.joinToString("\n")
}
fun hasUnique(unique: String) = uniques.contains(unique)
fun isGreatImprovement() = hasUnique("Great Improvement")
fun isRoad() = RoadStatus.values().any { it != RoadStatus.None && it.name == this.name }
fun isAncientRuinsEquivalent() = hasUnique("Provides a random bonus when entered")

View File

@ -225,6 +225,15 @@ class BaseUnit : INamed, INonPerpetualConstruction, ICivilopediaText {
}
) return true
// May buy [unitFilter] units for [amount] [Stat] [cityFilter] at an increasing price ([amount])
if (cityInfo != null && cityInfo.civInfo.getMatchingUniques("May buy [] units for [] [] [] at an increasing price ([])")
.any {
matchesFilter(it.params[0])
&& cityInfo.matchesFilter(it.params[3])
&& it.params[2] == stat.name
}
) return true
return super.canBePurchasedWithStat(cityInfo, stat)
}
@ -237,12 +246,12 @@ class BaseUnit : INamed, INonPerpetualConstruction, ICivilopediaText {
return (
sequenceOf(super.getBaseBuyCost(cityInfo, stat)).filterNotNull()
// May buy [unitFilter] units for [amount] [Stat] starting from the [eraName] at an increasing price ([amount])
+ cityInfo.civInfo.getMatchingUniques("May buy [] units for [] [] [] starting from the [] at an increasing price ([])")
+ (cityInfo.civInfo.getMatchingUniques("May buy [] units for [] [] [] starting from the [] at an increasing price ([])")
.filter {
matchesFilter(it.params[0])
&& cityInfo.matchesFilter(it.params[3])
&& cityInfo.civInfo.getEraNumber() >= ruleset.eras[it.params[4]]!!.eraNumber
&& it.params[2] == stat.name
&& cityInfo.matchesFilter(it.params[3])
&& cityInfo.civInfo.getEraNumber() >= ruleset.eras[it.params[4]]!!.eraNumber
&& it.params[2] == stat.name
}.map {
getCostForConstructionsIncreasingInPrice(
it.params[1].toInt(),
@ -250,6 +259,20 @@ class BaseUnit : INamed, INonPerpetualConstruction, ICivilopediaText {
cityInfo.civInfo.boughtConstructionsWithGloballyIncreasingPrice[name] ?: 0
)
}
)
+ (cityInfo.civInfo.getMatchingUniques("May buy [] units for [] [] [] at an increasing price ([])")
.filter {
matchesFilter(it.params[0])
&& cityInfo.matchesFilter(it.params[3])
&& it.params[2] == stat.name
}.map {
getCostForConstructionsIncreasingInPrice(
it.params[1].toInt(),
it.params[4].toInt(),
cityInfo.civInfo.boughtConstructionsWithGloballyIncreasingPrice[name] ?: 0
)
}
)
).minOrNull()
}

View File

@ -1,5 +1,6 @@
package com.unciv.models.ruleset.unit
import com.unciv.models.ruleset.IHasUniques
import com.unciv.models.ruleset.Unique
import com.unciv.models.stats.INamed
@ -16,13 +17,13 @@ enum class UnitMovementType { // The types of tiles the unit can by default ente
Air // Only city tiles and carrying units
}
class UnitType() : INamed {
class UnitType() : INamed, IHasUniques {
override lateinit var name: String
private var movementType: String? = null
private val unitMovementType: UnitMovementType? by lazy { if (movementType == null) null else UnitMovementType.valueOf(movementType!!) }
val uniques: ArrayList<String> = ArrayList()
val uniqueObjects: List<Unique> by lazy { uniques.map { Unique(it) } }
override var uniques: ArrayList<String> = ArrayList()
override val uniqueObjects: List<Unique> by lazy { uniques.map { Unique(it) } }
constructor(name: String, domain: String? = null) : this() {
this.name = name

View File

@ -2,6 +2,7 @@
package com.unciv.logic.map
import com.unciv.Constants
import com.unciv.logic.GameInfo
import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.civilization.diplomacy.DiplomacyManager
@ -37,6 +38,8 @@ class UnitMovementAlgorithmsTests {
name = "My nation"
cities = arrayListOf("The Capital")
}
civInfo.gameInfo = GameInfo()
civInfo.gameInfo.ruleSet = ruleSet
unit.civInfo = civInfo
@ -117,10 +120,13 @@ class UnitMovementAlgorithmsTests {
unit.baseUnit = BaseUnit().apply { unitType = type.key; ruleset = ruleSet }
unit.updateUniques()
Assert.assertTrue("$type cannot be in Ice", (
type.value.uniques.contains("Can enter ice tiles"))
|| type.value.uniques.contains("Can pass through impassable tiles"
) == unit.movement.canPassThrough(tile))
Assert.assertTrue(
"$type cannot be in Ice",
unit.movement.canPassThrough(tile) == (
type.value.uniques.contains("Can enter ice tiles")
|| type.value.uniques.contains("Can pass through impassable tiles")
)
)
}
}