Refactor more code, hopefully increasing maintainability (#5062)

* Fixed great person gift formula, confusing boolean, "great person" filter

* Refactored getRejectionReason to return a hashSet of reasons instead of a random one
This commit is contained in:
Xander Lenstra
2021-09-02 15:37:40 +02:00
committed by GitHub
parent 2e43637144
commit fcc335b78a
8 changed files with 408 additions and 225 deletions

View File

@ -1064,7 +1064,8 @@
"name": "Apollo Program",
"cost": 750,
"isNationalWonder": true,
"uniques": ["Enables construction of Spaceship parts", "Triggers a global alert upon completion"],
"uniques": ["Enables construction of Spaceship parts", "Triggers a global alert upon completion",
"Hidden when [Scientific] Victory is disabled"],
"requiredTech": "Rocketry"
},
@ -1091,7 +1092,7 @@
"name": "SS Cockpit",
"requiredResource": "Aluminum",
"requiredTech": "Satellites",
"uniques": ["Spaceship part", "Triggers a global alert upon completion", "Cannot be purchased"]
"uniques": ["Spaceship part", "Triggers a global alert upon completion", "Cannot be purchased", "Hidden when [Scientific] Victory is disabled"]
},
{
"name": "Hubble Space Telescope",
@ -1109,7 +1110,7 @@
"name": "SS Booster",
"requiredResource": "Aluminum",
"requiredTech": "Advanced Ballistics",
"uniques": ["Spaceship part", "Triggers a global alert upon completion", "Cannot be purchased"]
"uniques": ["Spaceship part", "Triggers a global alert upon completion", "Cannot be purchased", "Hidden when [Scientific] Victory is disabled"]
},
{
"name": "Spaceship Factory",
@ -1136,13 +1137,13 @@
"name": "SS Engine",
"requiredResource": "Aluminum",
"requiredTech": "Particle Physics",
"uniques": ["Spaceship part", "Triggers a global alert upon completion", "Cannot be purchased"]
"uniques": ["Spaceship part", "Triggers a global alert upon completion", "Cannot be purchased", "Hidden when [Scientific] Victory is disabled"]
},
{
"name": "SS Stasis Chamber",
"requiredResource": "Aluminum",
"requiredTech": "Nanotechnology",
"uniques": ["Spaceship part", "Triggers a global alert upon completion", "Cannot be purchased"]
"uniques": ["Spaceship part", "Triggers a global alert upon completion", "Cannot be purchased", "Hidden when [Scientific] Victory is disabled"]
},
// All Eras

View File

@ -375,14 +375,10 @@ class CityConstructions {
// Perpetual constructions should always still be valid (I hope)
if (construction is PerpetualConstruction) continue
val rejectionReason =
(construction as INonPerpetualConstruction).getRejectionReason(this)
val rejectionReasons =
(construction as INonPerpetualConstruction).getRejectionReasons(this)
if (rejectionReason.endsWith("lready built")
|| rejectionReason.startsWith("Cannot be built with")
|| rejectionReason.startsWith("Don't need to build any more")
|| rejectionReason.startsWith("Obsolete")
) {
if (rejectionReasons.hasAReasonToBeRemovedFromQueue()) {
if (construction is Building) {
// Production put into wonders gets refunded
if (construction.isWonder && getWorkDone(constructionName) != 0) {
@ -392,7 +388,7 @@ class CityConstructions {
}
} else if (construction is BaseUnit) {
// Production put into upgradable units gets put into upgraded version
if (rejectionReason.startsWith("Obsolete") && construction.upgradesTo != null) {
if (rejectionReasons.all { it == RejectionReason.Obsoleted } && construction.upgradesTo != null) {
// I'd love to use the '+=' operator but since 'inProgressConstructions[...]' can be null, kotlin doesn't allow me to
if (!inProgressConstructions.contains(construction.upgradesTo)) {
inProgressConstructions[construction.upgradesTo!!] = getWorkDone(constructionName)

View File

@ -21,7 +21,7 @@ interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques {
fun getProductionCost(civInfo: CivilizationInfo): Int
fun getStatBuyCost(cityInfo: CityInfo, stat: Stat): Int?
fun getRejectionReason(cityConstructions: CityConstructions): String
fun getRejectionReasons(cityConstructions: CityConstructions): RejectionReasons
fun postBuildEvent(cityConstructions: CityConstructions, boughtWith: Stat? = null): Boolean // Yes I'm hilarious.
fun getMatchingUniques(uniqueTemplate: String): Sequence<Unique> {
@ -31,26 +31,25 @@ interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques {
return uniqueObjects.any { it.placeholderText == uniqueTemplate }
}
fun canBePurchasedWithStat(cityInfo: CityInfo, stat: Stat, ignoreCityRequirements: Boolean = false): Boolean {
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
if (stat == Stat.Gold) return !uniques.contains("Unbuildable")
// Can be purchased with [Stat] [cityFilter]
if (getMatchingUniques("Can be purchased with [] []")
.any { it.params[0] == stat.name && (ignoreCityRequirements || cityInfo.matchesFilter(it.params[1])) }
.any { it.params[0] == stat.name && (cityInfo != null && cityInfo.matchesFilter(it.params[1])) }
) return true
// Can be purchased for [amount] [Stat] [cityFilter]
if (getMatchingUniques("Can be purchased for [] [] []")
.any { it.params[1] == stat.name && ( ignoreCityRequirements || cityInfo.matchesFilter(it.params[2])) }
.any { it.params[1] == stat.name && (cityInfo != null && cityInfo.matchesFilter(it.params[2])) }
) return true
return false
}
/** Checks if the construction should be purchasable, not whether it can be bought with a stat at all */
fun isPurchasable(cityConstructions: CityConstructions): Boolean {
val rejectionReason = getRejectionReason(cityConstructions)
return rejectionReason == ""
|| rejectionReason == "Can only be purchased"
val rejectionReasons = getRejectionReasons(cityConstructions)
return rejectionReasons.all { it == RejectionReason.Unbuildable }
}
fun canBePurchasedWithAnyStat(cityInfo: CityInfo): Boolean {
@ -81,6 +80,100 @@ 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
}
}
enum class RejectionReason(val shouldShow: Boolean, var errorMessage: String) {
AlreadyBuilt(false, "Building already built in this city"),
Unbuildable(false, "Unbuildable"),
CanOnlyBePurchased(true, "Can only be purchased"),
ShouldNotBeDisplayed(false, "Should not be displayed"),
DisabledBySetting(false, "Disabled by setting"),
HiddenWithoutVictory(false, "Hidden because a victory type has been disabled"),
MustBeOnTile(false, "Must be on a specific tile"),
MustNotBeOnTile(false, "Must not be on a specific tile"),
MustBeNextToTile(false, "Must be next to a specific tile"),
MustNotBeNextToTile(false, "Must not be next to a specific tile"),
MustOwnTile(false, "Must own a specific tile closeby"),
WaterUnitsInCoastalCities(false, "May only built water units in coastal cities"),
CanOnlyBeBuiltInSpecificCities(false, "Can only be built in specific cities"),
UniqueToOtherNation(false, "Unique to another nation"),
ReplacedByOurUnique(false, "Our unique replaces this"),
Obsoleted(false, "Obsolete"),
RequiresTech(false, "Required tech not researched"),
RequiresPolicy(false, "Requires a specific policy!"),
UnlockedWithEra(false, "Unlocked when reacing a specific era"),
MorePolicyBranches(false, "Hidden until more policy branches are fully adopted"),
RequiresNearbyResource(false, "Requires a certain resource being exploited nearby"),
InvalidRequiredBuilding(false, "Required building does not exist in ruleSet!"),
CannotBeBuiltWith(false, "Cannot be built at the same time as another building already built"),
RequiresBuildingInThisCity(true, "Requires a specific building in this city!"),
RequiresBuildingInAllCities(true, "Requires a specific building in all cities!"),
RequiresBuildingInSomeCity(true, "Requires a specific building anywhere in your empire!"),
WonderAlreadyBuilt(false, "Wonder already built"),
NationalWonderAlreadyBuilt(false, "National Wonder already built"),
WonderBeingBuiltElsewhere(true, "Wonder is being built elsewhere"),
NationalWonderBeingBuiltElsewhere(true, "National Wonder is being built elsewhere"),
CityStateWonder(false, "No Wonders for city-states"),
CityStateNationalWonder(false, "No National Wonders for city-states"),
WonderDisabledEra(false, "This Wonder is disabled when starting in this era"),
ReachedBuildCap(false, "Don't need to build any more of these!"),
ConsumesResources(true, "Consumes resources which you are lacking"),
PopulationRequirement(true, "Requires more population"),
NoSettlerForOneCityPlayers(false, "No settlers for city-states or one-city challangers");
}
open class PerpetualConstruction(override var name: String, val description: String) : IConstruction {
override fun shouldBeDisplayed(cityConstructions: CityConstructions) = isBuildable(cityConstructions)

View File

@ -927,10 +927,7 @@ class CivilizationInfo {
addNotification( "[${givingCityState.civName}] gave us a [${giftedUnit.name}] as a gift!", locations, givingCityState.civName, giftedUnit.name)
}
fun turnsForGreatPersonFromCityState(): Int = ((40 + -2 + Random().nextInt(5)) * gameInfo.gameParameters.gameSpeed.modifier).toInt()
// There seems to be some randomness in the amount of turns between receiving each great person,
// but I have no idea what the actual lower and upper bound are, so this is just an approximation
fun turnsForGreatPersonFromCityState(): Int = ((37 + Random().nextInt(7)) * gameInfo.gameParameters.gameSpeed.modifier).toInt()
fun getAllyCiv() = allyCivName

View File

@ -2,11 +2,10 @@ package com.unciv.models.ruleset
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.city.CityConstructions
import com.unciv.logic.city.CityInfo
import com.unciv.logic.city.INonPerpetualConstruction
import com.unciv.logic.city.*
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.models.Counter
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.TileImprovement
import com.unciv.models.stats.NamedStats
import com.unciv.models.stats.Stat
@ -228,7 +227,7 @@ class Building : NamedStats(), INonPerpetualConstruction, ICivilopediaText {
if (cost > 0) {
val stats = mutableListOf("$cost${Fonts.production}")
if (canBePurchasedWithStat(CityInfo(), Stat.Gold, true)) {
if (canBePurchasedWithStat(null, Stat.Gold)) {
stats += "${getBaseGoldCost(UncivGame.Current.gameInfo.currentPlayerCiv).toInt() / 10 * 10}${Fonts.gold}"
}
textList += FormattedLine(stats.joinToString(", ", "{Cost}: "))
@ -352,13 +351,13 @@ class Building : NamedStats(), INonPerpetualConstruction, ICivilopediaText {
}
override fun canBePurchasedWithStat(cityInfo: CityInfo, stat: Stat, ignoreCityRequirements: Boolean): Boolean {
override fun canBePurchasedWithStat(cityInfo: CityInfo?, stat: Stat): Boolean {
if (stat == Stat.Gold && isAnyWonder()) return false
// May buy [buildingFilter] buildings for [amount] [Stat] [cityFilter]
if (!ignoreCityRequirements && cityInfo.getMatchingUniques("May buy [] buildings for [] [] []")
if (cityInfo != null && cityInfo.getMatchingUniques("May buy [] buildings for [] [] []")
.any { it.params[2] == stat.name && matchesFilter(it.params[0]) && cityInfo.matchesFilter(it.params[3]) }
) return true
return super.canBePurchasedWithStat(cityInfo, stat, ignoreCityRequirements)
return super.canBePurchasedWithStat(cityInfo, stat)
}
override fun getBaseBuyCost(cityInfo: CityInfo, stat: Stat): Int? {
@ -408,192 +407,264 @@ class Building : NamedStats(), INonPerpetualConstruction, ICivilopediaText {
override fun shouldBeDisplayed(cityConstructions: CityConstructions): Boolean {
if (cityConstructions.isBeingConstructedOrEnqueued(name))
return false
val rejectionReason = getRejectionReason(cityConstructions)
return rejectionReason == ""
|| rejectionReason.startsWith("Requires")
|| rejectionReason.startsWith("Consumes")
|| rejectionReason.endsWith("Wonder is being built elsewhere")
|| rejectionReason == "Can only be purchased"
val rejectionReasons = getRejectionReasons(cityConstructions)
return rejectionReasons.none { !it.shouldShow }
|| (
canBePurchasedWithAnyStat(cityConstructions.cityInfo)
&& rejectionReasons.all { it == RejectionReason.Unbuildable }
)
}
override fun getRejectionReason(construction: CityConstructions): String {
if (construction.isBuilt(name)) return "Already built"
// for buildings that are created as side effects of other things, and not directly built
// unless they can be bought with faith
if (uniques.contains("Unbuildable")) {
if (canBePurchasedWithAnyStat(construction.cityInfo))
return "Can only be purchased"
return "Unbuildable"
}
override fun getRejectionReasons(cityConstructions: CityConstructions): RejectionReasons {
val rejectionReasons = RejectionReasons()
val cityCenter = cityConstructions.cityInfo.getCenterTile()
val civInfo = cityConstructions.cityInfo.civInfo
val ruleSet = civInfo.gameInfo.ruleSet
if (cityConstructions.isBuilt(name))
rejectionReasons.add(RejectionReason.AlreadyBuilt)
// for buildings that are created as side effects of other things, and not directly built,
// or for buildings that can only be bought
if (uniques.contains("Unbuildable"))
rejectionReasons.add(RejectionReason.Unbuildable)
val cityCenter = construction.cityInfo.getCenterTile()
val civInfo = construction.cityInfo.civInfo
// This overrides the others
if (uniqueObjects
.any {
it.placeholderText == "Not displayed as an available construction unless [] is built"
&& !construction.containsBuildingOrEquivalent(it.params[0])
for (unique in uniqueObjects) {
when (unique.placeholderText) {
// Deprecated since 3.16.11, replace with "Not displayed [...] construction without []"
"Not displayed as an available construction unless [] is built" ->
if (!cityConstructions.containsBuildingOrEquivalent(unique.params[0]))
rejectionReasons.add(RejectionReason.ShouldNotBeDisplayed)
//
"Not displayed as an available construction without []" ->
if (unique.params[0] in ruleSet.tileResources && !civInfo.hasResource(unique.params[0])
|| unique.params[0] in ruleSet.buildings && !cityConstructions.containsBuildingOrEquivalent(unique.params[0])
|| unique.params[0] in ruleSet.technologies && !civInfo.tech.isResearched(unique.params[0])
|| unique.params[0] in ruleSet.policies && !civInfo.policies.isAdopted(unique.params[0])
)
rejectionReasons.add(RejectionReason.ShouldNotBeDisplayed)
"Enables nuclear weapon" -> if (!cityConstructions.cityInfo.civInfo.gameInfo.gameParameters.nuclearWeaponsEnabled)
rejectionReasons.add(RejectionReason.DisabledBySetting)
"Must be on []" ->
if (!cityCenter.matchesTerrainFilter(unique.params[0], civInfo))
rejectionReasons.add(RejectionReason.MustBeOnTile.apply { errorMessage = unique.text })
"Must not be on []" ->
if (cityCenter.matchesTerrainFilter(unique.params[0], civInfo))
rejectionReasons.add(RejectionReason.MustNotBeOnTile.apply { errorMessage = unique.text })
"Must be next to []" ->
if (// Fresh water is special, in that rivers are not tiles themselves but also fit the filter.
!(unique.params[0] == "Fresh water" && cityCenter.isAdjacentToRiver())
&& cityCenter.getTilesInDistance(1).none { it.matchesFilter(unique.params[0], civInfo) }
)
rejectionReasons.add(RejectionReason.MustBeNextToTile.apply { errorMessage = unique.text })
"Must not be next to []" ->
if (cityCenter.getTilesInDistance(1).any { it.matchesFilter(unique.params[0], civInfo) })
rejectionReasons.add(RejectionReason.MustNotBeNextToTile.apply { errorMessage = unique.text })
"Must have an owned [] within [] tiles" ->
if (cityCenter.getTilesInDistance(unique.params[1].toInt())
.none { it.matchesFilter(unique.params[0], civInfo) && it.getOwner() == cityConstructions.cityInfo.civInfo }
)
rejectionReasons.add(RejectionReason.MustOwnTile.apply { errorMessage = unique.text })
// Deprecated since 3.16.11
"Can only be built in annexed cities" ->
if (
cityConstructions.cityInfo.isPuppet
|| cityConstructions.cityInfo.civInfo.civName == cityConstructions.cityInfo.foundingCiv
)
rejectionReasons.add(RejectionReason.CanOnlyBeBuiltInSpecificCities.apply { errorMessage = unique.text })
//
"Can only be built []" ->
if (!cityConstructions.cityInfo.matchesFilter(unique.params[0]))
rejectionReasons.add(RejectionReason.CanOnlyBeBuiltInSpecificCities.apply { errorMessage = unique.text })
"Obsolete with []" ->
if (civInfo.tech.isResearched(unique.params[0]))
rejectionReasons.add(RejectionReason.Obsoleted.apply { errorMessage = unique.text })
Constants.hiddenWithoutReligionUnique ->
if (!civInfo.gameInfo.hasReligionEnabled())
rejectionReasons.add(RejectionReason.DisabledBySetting)
}
) return "Should not be displayed"
for (unique in uniqueObjects.filter { it.placeholderText == "Not displayed as an available construction without []" }) {
val filter = unique.params[0]
if (filter in civInfo.gameInfo.ruleSet.tileResources && !construction.cityInfo.civInfo.hasResource(filter)
|| filter in civInfo.gameInfo.ruleSet.buildings && !construction.containsBuildingOrEquivalent(filter))
return "Should not be displayed"
}
for (unique in uniqueObjects) when (unique.placeholderText) {
"Enables nuclear weapon" -> if(!construction.cityInfo.civInfo.gameInfo.gameParameters.nuclearWeaponsEnabled) return "Disabled by setting"
"Must be on []" -> if (!cityCenter.matchesTerrainFilter(unique.params[0], civInfo)) return unique.text
"Must not be on []" -> if (cityCenter.matchesTerrainFilter(unique.params[0], civInfo)) return unique.text
"Must be next to []" -> if (!(unique.params[0] == "Fresh water" && cityCenter.isAdjacentToRiver()) // Fresh water is special, in that rivers are not tiles themselves but also fit the filter.
&& cityCenter.getTilesInDistance(1).none { it.matchesFilter(unique.params[0], civInfo) }) return unique.text
"Must not be next to []" -> if (cityCenter.getTilesInDistance(1).any { it.matchesFilter(unique.params[0], civInfo) }) return unique.text
"Must have an owned [] within [] tiles" -> if (cityCenter.getTilesInDistance(unique.params[1].toInt()).none {
it.matchesFilter(unique.params[0], civInfo) && it.getOwner() == construction.cityInfo.civInfo
}) return unique.text
// Deprecated since 3.16.11
"Can only be built in annexed cities" -> if (construction.cityInfo.isPuppet
|| construction.cityInfo.civInfo.civName == construction.cityInfo.foundingCiv) return unique.text
//
"Can only be built []" -> if (!construction.cityInfo.matchesFilter(unique.params[0])) return unique.text
"Obsolete with []" -> if (civInfo.tech.isResearched(unique.params[0])) return unique.text
Constants.hiddenWithoutReligionUnique -> if (!civInfo.gameInfo.hasReligionEnabled()) return unique.text
}
if (uniqueTo != null && uniqueTo != civInfo.civName) return "Unique to $uniqueTo"
if (uniqueTo != null && uniqueTo != civInfo.civName)
rejectionReasons.add(RejectionReason.UniqueToOtherNation.apply { errorMessage = "Unique to $uniqueTo"})
if (civInfo.gameInfo.ruleSet.buildings.values.any { it.uniqueTo == civInfo.civName && it.replaces == name })
return "Our unique building replaces this"
if (requiredTech != null && !civInfo.tech.isResearched(requiredTech!!)) return "$requiredTech not researched"
rejectionReasons.add(RejectionReason.ReplacedByOurUnique)
if (requiredTech != null && !civInfo.tech.isResearched(requiredTech!!))
rejectionReasons.add(RejectionReason.RequiresTech.apply { "$requiredTech not researched!"})
for (unique in uniqueObjects.filter { it.placeholderText == "Unlocked with []" })
if (civInfo.tech.researchedTechnologies.none { it.era() == unique.params[0] || it.name == unique.params[0] }
&& !civInfo.policies.isAdopted(unique.params[0]))
return unique.text
for (unique in uniqueObjects) {
if (unique.placeholderText != "Unlocked with []" && unique.placeholderText != "Requires []") continue
val filter = unique.params[0]
when {
ruleSet.technologies.contains(filter) ->
if (!civInfo.tech.isResearched(filter))
rejectionReasons.add(RejectionReason.RequiresTech.apply { errorMessage = unique.text })
ruleSet.policies.contains(filter) ->
if (!civInfo.policies.isAdopted(filter))
rejectionReasons.add(RejectionReason.RequiresPolicy.apply { errorMessage = unique.text })
// ToDo: Fix this when eras.json is required
ruleSet.getEraNumber(filter) != -1 ->
if (civInfo.getEraNumber() < ruleSet.getEraNumber(filter))
rejectionReasons.add(RejectionReason.UnlockedWithEra.apply { errorMessage = unique.text })
ruleSet.buildings.contains(filter) ->
if (civInfo.cities.none { it.cityConstructions.containsBuildingOrEquivalent(filter) })
rejectionReasons.add(RejectionReason.RequiresBuildingInSomeCity.apply { errorMessage = unique.text })
}
}
// Regular wonders
if (isWonder) {
if (civInfo.gameInfo.getCities().any { it.cityConstructions.isBuilt(name) })
return "Wonder is already built"
rejectionReasons.add(RejectionReason.WonderAlreadyBuilt)
if (civInfo.cities.any { it != construction.cityInfo && it.cityConstructions.isBeingConstructedOrEnqueued(name) })
return "Wonder is being built elsewhere"
if (civInfo.cities.any { it != cityConstructions.cityInfo && it.cityConstructions.isBeingConstructedOrEnqueued(name) })
rejectionReasons.add(RejectionReason.WonderBeingBuiltElsewhere)
if (civInfo.isCityState())
return "No world wonders for city-states"
rejectionReasons.add(RejectionReason.CityStateWonder)
val ruleSet = civInfo.gameInfo.ruleSet
val startingEra = civInfo.gameInfo.gameParameters.startingEra
if (startingEra in ruleSet.eras && name in ruleSet.eras[startingEra]!!.startingObsoleteWonders)
return "Wonder is disabled when starting in this era"
rejectionReasons.add(RejectionReason.WonderDisabledEra)
}
// National wonders
if (isNationalWonder) {
if (civInfo.cities.any { it.cityConstructions.isBuilt(name) })
return "National Wonder is already built"
if (requiredBuildingInAllCities != null && civInfo.gameInfo.ruleSet.buildings[requiredBuildingInAllCities!!] == null)
return "Required building in all cities does not exist in the ruleset!"
if (requiredBuildingInAllCities != null
rejectionReasons.add(RejectionReason.NationalWonderAlreadyBuilt)
if (requiredBuildingInAllCities != null && civInfo.gameInfo.ruleSet.buildings[requiredBuildingInAllCities!!] == null) {
rejectionReasons.add(RejectionReason.InvalidRequiredBuilding)
} else {
if (requiredBuildingInAllCities != null
&& civInfo.cities.any {
!it.isPuppet && !it.cityConstructions
.containsBuildingOrEquivalent(requiredBuildingInAllCities!!)
})
return "Requires a [${civInfo.getEquivalentBuilding(requiredBuildingInAllCities!!)}] in all cities"
if (civInfo.cities.any { it != construction.cityInfo && it.cityConstructions.isBeingConstructedOrEnqueued(name) })
return "National Wonder is being built elsewhere"
if (civInfo.isCityState())
return "No national wonders for city-states"
.containsBuildingOrEquivalent(requiredBuildingInAllCities!!)
}
) {
rejectionReasons.add(RejectionReason.RequiresBuildingInAllCities
.apply { errorMessage = "Requires a [${civInfo.getEquivalentBuilding(requiredBuildingInAllCities!!)}] in all cities"})
}
if (civInfo.cities.any { it != cityConstructions.cityInfo && it.cityConstructions.isBeingConstructedOrEnqueued(name) })
rejectionReasons.add(RejectionReason.NationalWonderBeingBuiltElsewhere)
if (civInfo.isCityState())
rejectionReasons.add(RejectionReason.CityStateNationalWonder)
}
}
if ("Spaceship part" in uniques) {
if (!civInfo.hasUnique("Enables construction of Spaceship parts")) return "Apollo project not built!"
if (civInfo.victoryManager.unconstructedSpaceshipParts()[name] == 0) return "Don't need to build any more of these!"
if (!civInfo.hasUnique("Enables construction of Spaceship parts"))
rejectionReasons.add(
RejectionReason.RequiresBuildingInSomeCity.apply { errorMessage = "Apollo project not built!" }
)
if (civInfo.victoryManager.unconstructedSpaceshipParts()[name] == 0)
rejectionReasons.add(RejectionReason.ReachedBuildCap)
}
for (unique in uniqueObjects) when (unique.placeholderText) {
"Requires []" -> {
val filter = unique.params[0]
if (filter in civInfo.gameInfo.ruleSet.buildings) {
if (civInfo.cities.none { it.cityConstructions.containsBuildingOrEquivalent(filter) }) return unique.text // Wonder is not built
} else if (!civInfo.policies.isAdopted(filter)) return "Policy is not adopted" // this reason should not be displayed
}
"Requires a [] in this city" -> {
val filter = unique.params[0]
if (civInfo.gameInfo.ruleSet.buildings.containsKey(filter)
&& !construction.containsBuildingOrEquivalent(filter))
return "Requires a [${civInfo.getEquivalentBuilding(filter)}] in this city" // replace with civ-specific building for user
if (civInfo.gameInfo.ruleSet.buildings.containsKey(filter) && !cityConstructions.containsBuildingOrEquivalent(filter))
rejectionReasons.add(
// replace with civ-specific building for user
RejectionReason.RequiresBuildingInThisCity.apply { errorMessage = "Requires a [${civInfo.getEquivalentBuilding(filter)}] in this city" }
)
}
"Requires a [] in all cities" -> {
val filter = unique.params[0]
if (civInfo.gameInfo.ruleSet.buildings.containsKey(filter)
&& civInfo.cities.any { !it.isPuppet && !it.cityConstructions.containsBuildingOrEquivalent(unique.params[0]) })
return "Requires a [${civInfo.getEquivalentBuilding(unique.params[0])}] in all cities" // replace with civ-specific building for user
}
"Hidden until [] social policy branches have been completed" -> {
if (construction.cityInfo.civInfo.getCompletedPolicyBranchesCount() < unique.params[0].toInt()) {
return "Should not be displayed"
if (civInfo.gameInfo.ruleSet.buildings.containsKey(filter)
&& civInfo.cities.any {
!it.isPuppet && !it.cityConstructions.containsBuildingOrEquivalent(unique.params[0])
}
) {
rejectionReasons.add(
// replace with civ-specific building for user
RejectionReason.RequiresBuildingInAllCities.apply {
errorMessage = "Requires a [${civInfo.getEquivalentBuilding(unique.params[0])}] in all cities"
}
)
}
}
"Hidden until [] social policy branches have been completed" -> {
if (cityConstructions.cityInfo.civInfo.getCompletedPolicyBranchesCount() < unique.params[0].toInt())
rejectionReasons.add(RejectionReason.MorePolicyBranches.apply { errorMessage = unique.text })
}
"Hidden when [] Victory is disabled" -> {
if (!civInfo.gameInfo.gameParameters.victoryTypes.contains(VictoryType.valueOf(unique.params[0]))) {
return unique.text
}
if (!civInfo.gameInfo.gameParameters.victoryTypes.contains(VictoryType.valueOf(unique.params[0])))
rejectionReasons.add(RejectionReason.HiddenWithoutVictory.apply { errorMessage = unique.text })
}
// Deprecated since 3.15.14
"Hidden when cultural victory is disabled" -> {
if (!civInfo.gameInfo.gameParameters.victoryTypes.contains(VictoryType.Cultural)) {
return unique.text
}
if (!civInfo.gameInfo.gameParameters.victoryTypes.contains(VictoryType.Cultural))
rejectionReasons.add(RejectionReason.HiddenWithoutVictory.apply { errorMessage = unique.text })
}
//
}
if (requiredBuilding != null && !construction.containsBuildingOrEquivalent(requiredBuilding!!)) {
if (!civInfo.gameInfo.ruleSet.buildings.containsKey(requiredBuilding!!))
return "Requires a [${requiredBuilding}] in this city, which doesn't seem to exist in this ruleset!"
return "Requires a [${civInfo.getEquivalentBuilding(requiredBuilding!!)}] in this city"
if (requiredBuilding != null && !cityConstructions.containsBuildingOrEquivalent(requiredBuilding!!)) {
if (!civInfo.gameInfo.ruleSet.buildings.containsKey(requiredBuilding!!)) {
rejectionReasons.add(
RejectionReason.InvalidRequiredBuilding
.apply { errorMessage = "Requires a [${requiredBuilding}] in this city, which doesn't seem to exist in this ruleset!" }
)
} else {
rejectionReasons.add(
RejectionReason.RequiresBuildingInThisCity.apply { errorMessage = "Requires a [${civInfo.getEquivalentBuilding(requiredBuilding!!)}] in this city"}
)
}
}
// cannotBeBuiltWith is Deprecated as of 3.15.19
val cannotBeBuiltWith = uniqueObjects
.firstOrNull { it.placeholderText == "Cannot be built with []" }
?.params?.get(0)
?: this.cannotBeBuiltWith
if (cannotBeBuiltWith != null && construction.isBuilt(cannotBeBuiltWith))
return "Cannot be built with [$cannotBeBuiltWith]"
if (cannotBeBuiltWith != null && cityConstructions.isBuilt(cannotBeBuiltWith))
rejectionReasons.add(RejectionReason.CannotBeBuiltWith.apply { errorMessage = "Cannot be built with [$cannotBeBuiltWith]" })
for ((resource, amount) in getResourceRequirements())
if (civInfo.getCivResourcesByName()[resource]!! < amount) {
return if (amount == 1) "Consumes 1 [$resource]" // Again, to preserve existing translations
else "Consumes [$amount] [$resource]"
rejectionReasons.add(RejectionReason.ConsumesResources.apply {
errorMessage = "Consumes [$amount] [$resource]"
})
}
if (requiredNearbyImprovedResources != null) {
val containsResourceWithImprovement = construction.cityInfo.getWorkableTiles()
.any {
it.resource != null
&& requiredNearbyImprovedResources!!.contains(it.resource!!)
&& it.getOwner() == civInfo
&& (it.getTileResource().improvement == it.improvement || it.getTileImprovement()?.isGreatImprovement() == true || it.isCityCenter())
}
if (!containsResourceWithImprovement) return "Nearby $requiredNearbyImprovedResources required"
val containsResourceWithImprovement = cityConstructions.cityInfo.getWorkableTiles()
.any {
it.resource != null
&& requiredNearbyImprovedResources!!.contains(it.resource!!)
&& it.getOwner() == civInfo
&& (it.getTileResource().improvement == it.improvement || it.isCityCenter()
|| (it.getTileImprovement()?.isGreatImprovement() == true && it.getTileResource().resourceType == ResourceType.Strategic)
)
}
if (!containsResourceWithImprovement)
rejectionReasons.add(RejectionReason.RequiresNearbyResource.apply { errorMessage = "Nearby $requiredNearbyImprovedResources required" })
}
if (!civInfo.gameInfo.gameParameters.victoryTypes.contains(VictoryType.Scientific)
&& "Enables construction of Spaceship parts" in uniques)
return "Can't construct spaceship parts if scientific victory is not enabled!"
return ""
return rejectionReasons
}
override fun isBuildable(cityConstructions: CityConstructions): Boolean =
getRejectionReason(cityConstructions) == ""
getRejectionReasons(cityConstructions).isEmpty()
override fun postBuildEvent(cityConstructions: CityConstructions, boughtWith: Stat?): Boolean {
val civInfo = cityConstructions.cityInfo.civInfo

View File

@ -2,9 +2,7 @@ package com.unciv.models.ruleset.unit
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.city.CityConstructions
import com.unciv.logic.city.CityInfo
import com.unciv.logic.city.INonPerpetualConstruction
import com.unciv.logic.city.*
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.MapUnit
import com.unciv.models.ruleset.Ruleset
@ -126,7 +124,7 @@ class BaseUnit : INamed, INonPerpetualConstruction, ICivilopediaText {
if (cost > 0) {
stats.clear()
stats += "$cost${Fonts.production}"
if (canBePurchasedWithStat(CityInfo(), Stat.Gold, true))
if (canBePurchasedWithStat(null, Stat.Gold))
stats += "${getBaseGoldCost(UncivGame.Current.gameInfo.currentPlayerCiv).toInt() / 10 * 10}${Fonts.gold}"
textList += FormattedLine(stats.joinToString(", ", "{Cost}: "))
}
@ -216,13 +214,9 @@ class BaseUnit : INamed, INonPerpetualConstruction, ICivilopediaText {
return productionCost.toInt()
}
override fun canBePurchasedWithStat(
cityInfo: CityInfo,
stat: Stat,
ignoreCityRequirements: Boolean
): Boolean {
override fun canBePurchasedWithStat(cityInfo: CityInfo?, stat: Stat): Boolean {
// May buy [unitFilter] units for [amount] [Stat] starting from the [eraName] at an increasing price ([amount])
if (cityInfo.civInfo.getMatchingUniques("May buy [] units for [] [] [] starting from the [] at an increasing price ([])")
if (cityInfo != null && cityInfo.civInfo.getMatchingUniques("May buy [] units for [] [] [] starting from the [] at an increasing price ([])")
.any {
matchesFilter(it.params[0])
&& cityInfo.matchesFilter(it.params[3])
@ -231,7 +225,7 @@ class BaseUnit : INamed, INonPerpetualConstruction, ICivilopediaText {
}
) return true
return super.canBePurchasedWithStat(cityInfo, stat, ignoreCityRequirements)
return super.canBePurchasedWithStat(cityInfo, stat)
}
private fun getCostForConstructionsIncreasingInPrice(baseCost: Int, increaseCost: Int, previouslyBought: Int): Int {
@ -285,84 +279,103 @@ class BaseUnit : INamed, INonPerpetualConstruction, ICivilopediaText {
fun getDisbandGold(civInfo: CivilizationInfo) = getBaseGoldCost(civInfo).toInt() / 20
override fun shouldBeDisplayed(cityConstructions: CityConstructions): Boolean {
val rejectionReason = getRejectionReason(cityConstructions)
return rejectionReason == ""
|| rejectionReason.startsWith("Requires")
|| rejectionReason.startsWith("Consumes")
|| rejectionReason == "Can only be purchased"
val rejectionReasons = getRejectionReasons(cityConstructions)
return rejectionReasons.none { !it.shouldShow }
|| (
canBePurchasedWithAnyStat(cityConstructions.cityInfo)
&& rejectionReasons.all { it == RejectionReason.Unbuildable }
)
}
override fun getRejectionReason(cityConstructions: CityConstructions): String {
override fun getRejectionReasons(cityConstructions: CityConstructions): RejectionReasons {
val rejectionReasons = RejectionReasons()
if (isWaterUnit() && !cityConstructions.cityInfo.isCoastal())
return "Can only build water units in coastal cities"
rejectionReasons.add(RejectionReason.WaterUnitsInCoastalCities)
val civInfo = cityConstructions.cityInfo.civInfo
for (unique in uniqueObjects.filter { it.placeholderText == "Not displayed as an available construction without []" }) {
val filter = unique.params[0]
if (filter in civInfo.gameInfo.ruleSet.tileResources && !civInfo.hasResource(filter)
|| filter in civInfo.gameInfo.ruleSet.buildings && !cityConstructions.containsBuildingOrEquivalent(filter))
return "Should not be displayed"
rejectionReasons.add(RejectionReason.ShouldNotBeDisplayed)
}
val civRejectionReason = getRejectionReason(civInfo)
if (civRejectionReason != "") {
if (civRejectionReason == "Unbuildable" && canBePurchasedWithAnyStat(cityConstructions.cityInfo))
return "Can only be purchased"
return civRejectionReason
val civRejectionReasons = getRejectionReasons(civInfo)
if (civRejectionReasons.isNotEmpty()) {
rejectionReasons.addAll(civRejectionReasons)
}
for (unique in uniqueObjects.filter { it.placeholderText == "Requires at least [] population" })
if (unique.params[0].toInt() > cityConstructions.cityInfo.population.population)
return unique.text
return ""
rejectionReasons.add(RejectionReason.PopulationRequirement)
return rejectionReasons
}
/** @param ignoreTechPolicyRequirements: its `true` value is used when upgrading via ancient ruins,
* as there we don't care whether we have the required tech, policy or building for the unit,
* but do still care whether we have the resources required for the unit
*/
fun getRejectionReason(civInfo: CivilizationInfo, ignoreTechPolicyRequirements: Boolean = false): String {
if (uniques.contains("Unbuildable")) return "Unbuildable"
if (!ignoreTechPolicyRequirements && requiredTech != null && !civInfo.tech.isResearched(requiredTech!!)) return "$requiredTech not researched"
if (!ignoreTechPolicyRequirements && obsoleteTech != null && civInfo.tech.isResearched(obsoleteTech!!)) return "Obsolete by $obsoleteTech"
if (uniqueTo != null && uniqueTo != civInfo.civName) return "Unique to $uniqueTo"
if (civInfo.gameInfo.ruleSet.units.values.any { it.uniqueTo == civInfo.civName && it.replaces == name })
return "Our unique unit replaces this"
if (!civInfo.gameInfo.gameParameters.nuclearWeaponsEnabled && isNuclearWeapon()
) return "Disabled by setting"
fun getRejectionReasons(civInfo: CivilizationInfo): RejectionReasons {
val rejectionReasons = RejectionReasons()
val ruleSet = civInfo.gameInfo.ruleSet
if (uniques.contains("Unbuildable"))
rejectionReasons.add(RejectionReason.Unbuildable)
if (requiredTech != null && !civInfo.tech.isResearched(requiredTech!!))
rejectionReasons.add(RejectionReason.RequiresTech.apply { this.errorMessage = "$requiredTech not researched" })
if (obsoleteTech != null && civInfo.tech.isResearched(obsoleteTech!!))
rejectionReasons.add(RejectionReason.Obsoleted.apply { this.errorMessage = "Obsolete by $obsoleteTech" })
if (uniqueTo != null && uniqueTo != civInfo.civName)
rejectionReasons.add(RejectionReason.UniqueToOtherNation.apply { this.errorMessage = "Unique to $uniqueTo" })
if (ruleSet.units.values.any { it.uniqueTo == civInfo.civName && it.replaces == name })
rejectionReasons.add(RejectionReason.ReplacedByOurUnique.apply { this.errorMessage = "Our unique unit replaces this" })
if (!civInfo.gameInfo.gameParameters.nuclearWeaponsEnabled && isNuclearWeapon())
rejectionReasons.add(RejectionReason.DisabledBySetting)
for (unique in uniqueObjects.filter { it.placeholderText == "Unlocked with []" })
// ToDo: Clean this up when eras.json is required
if ((civInfo.gameInfo.ruleSet.getEraNumber(unique.params[0]) != -1 && civInfo.getEraNumber() >= civInfo.gameInfo.ruleSet.getEraNumber(unique.params[0]))
|| civInfo.hasTechOrPolicy(unique.params[0])
) return unique.text
for (unique in uniqueObjects.filter { it.placeholderText == "Requires []" }) {
for (unique in uniqueObjects) {
if (unique.placeholderText != "Unlocked with []" && unique.placeholderText != "Requires []") continue
val filter = unique.params[0]
if (!ignoreTechPolicyRequirements && filter in civInfo.gameInfo.ruleSet.buildings) {
if (civInfo.cities.none { it.cityConstructions.containsBuildingOrEquivalent(filter) }) return unique.text // Wonder is not built
} else if (!ignoreTechPolicyRequirements && !civInfo.policies.isAdopted(filter)) return "Policy is not adopted"
when {
ruleSet.technologies.contains(filter) ->
if (!civInfo.tech.isResearched(filter))
rejectionReasons.add(RejectionReason.RequiresTech.apply { errorMessage = unique.text })
ruleSet.policies.contains(filter) ->
if (!civInfo.policies.isAdopted(filter))
rejectionReasons.add(RejectionReason.RequiresPolicy.apply { errorMessage = unique.text })
// ToDo: Fix this when eras.json is required
ruleSet.getEraNumber(filter) != -1 ->
if (civInfo.getEraNumber() < ruleSet.getEraNumber(filter))
rejectionReasons.add(RejectionReason.UnlockedWithEra.apply { errorMessage = unique.text })
ruleSet.buildings.contains(filter) ->
if (civInfo.cities.none { it.cityConstructions.containsBuildingOrEquivalent(filter) })
rejectionReasons.add(RejectionReason.RequiresBuildingInSomeCity.apply { errorMessage = unique.text })
}
}
for ((resource, amount) in getResourceRequirements())
if (civInfo.getCivResourcesByName()[resource]!! < amount) {
return if (amount == 1) "Consumes 1 [$resource]" // Again, to preserve existing translations
else "Consumes [$amount] [$resource]"
rejectionReasons.add(RejectionReason.ConsumesResources.apply {
errorMessage = "Consumes [$amount] [$resource]"
})
}
if (uniques.contains(Constants.settlerUnique) && civInfo.isCityState()) return "No settler for city-states"
if (uniques.contains(Constants.settlerUnique) && civInfo.isOneCityChallenger()) return "No settler for players in One City Challenge"
return ""
if (uniques.contains(Constants.settlerUnique) &&
(civInfo.isCityState() || civInfo.isOneCityChallenger())
)
rejectionReasons.add(RejectionReason.NoSettlerForOneCityPlayers)
return rejectionReasons
}
fun isBuildable(civInfo: CivilizationInfo) = getRejectionReason(civInfo) == ""
fun isBuildable(civInfo: CivilizationInfo) = getRejectionReasons(civInfo).isEmpty()
override fun isBuildable(cityConstructions: CityConstructions): Boolean {
return getRejectionReason(cityConstructions) == ""
return getRejectionReasons(cityConstructions).isEmpty()
}
fun isBuildableIgnoringTechs(civInfo: CivilizationInfo): Boolean {
val rejectionReasons = getRejectionReasons(civInfo)
return rejectionReasons.filterTechPolicyEraWonderRequirements().isEmpty()
}
/** Preemptively as in: buildable without actually having the tech and/or policy required for it.
* Still checks for resource use and other things
*/
fun isBuildableIgnoringTechs(civInfo: CivilizationInfo) =
getRejectionReason(civInfo, true) == ""
override fun postBuildEvent(cityConstructions: CityConstructions, boughtWith: Stat?): Boolean {
val civInfo = cityConstructions.cityInfo.civInfo
@ -466,8 +479,10 @@ class BaseUnit : INamed, INonPerpetualConstruction, ICivilopediaText {
"non-air" -> !movesLikeAirUnits()
"Nuclear Weapon" -> isNuclearWeapon()
"Great Person", "Great" -> isGreatPerson()
// Deprecated as of 3.15.2
"military water" -> isMilitary() && isWaterUnit()
"military water" -> isMilitary() && isWaterUnit()
//
else -> {
if (getType().matchesFilter(filter)) return true
if (

View File

@ -165,19 +165,30 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
val useStoredProduction = entry is Building || !cityConstructions.isBeingConstructedOrEnqueued(entry.name)
var buttonText = entry.name.tr() + cityConstructions.getTurnsToConstructionString(entry.name, useStoredProduction)
for ((resource, amount) in entry.getResourceRequirements()) {
buttonText += "\n" + (if (amount == 1) "Consumes 1 [$resource]"
else "Consumes [$amount] [$resource]").tr()
buttonText += "\n" + (
if (amount == 1) "Consumes 1 [$resource]"
else "Consumes [$amount] [$resource]"
).tr()
}
constructionButtonDTOList.add(ConstructionButtonDTO(entry, buttonText,
entry.getRejectionReason(cityConstructions)))
constructionButtonDTOList.add(
ConstructionButtonDTO(
entry,
buttonText,
entry.getRejectionReasons(cityConstructions).getMostImportantRejectionReason()
)
)
}
for (specialConstruction in PerpetualConstruction.perpetualConstructionsMap.values
.filter { it.shouldBeDisplayed(cityConstructions) }) {
constructionButtonDTOList.add(ConstructionButtonDTO(specialConstruction,
"Produce [${specialConstruction.name}]".tr()
+ specialConstruction.getProductionTooltip(city)))
.filter { it.shouldBeDisplayed(cityConstructions) }
) {
constructionButtonDTOList.add(
ConstructionButtonDTO(
specialConstruction,
"Produce [${specialConstruction.name}]".tr() + specialConstruction.getProductionTooltip(city)
)
)
}
return constructionButtonDTOList
@ -297,7 +308,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
Color.BROWN.cpy().lerp(Color.WHITE, 0.5f), Color.WHITE)
}
private class ConstructionButtonDTO(val construction: IConstruction, val buttonText: String, val rejectionReason: String = "")
private class ConstructionButtonDTO(val construction: IConstruction, val buttonText: String, val rejectionReason: String? = null)
private fun getConstructionButton(constructionButtonDTO: ConstructionButtonDTO): Table {
val construction = constructionButtonDTO.construction
@ -325,7 +336,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
pickConstructionButton.row()
// no rejection reason means we can build it!
if (constructionButtonDTO.rejectionReason != "") {
if (constructionButtonDTO.rejectionReason != null) {
pickConstructionButton.color = Color.GRAY
pickConstructionButton.add(constructionButtonDTO.rejectionReason.toLabel(Color.RED).apply { wrap = true })
.colspan(pickConstructionButton.columns).fillX().left().padTop(2f)

View File

@ -769,8 +769,7 @@ object UnitActions {
val giftAction = {
if (recipient.isCityState()) {
for (unique in unit.civInfo.getMatchingUniques("Gain [] Influence with a [] gift to a City-State")) {
if ((unit.isGreatPerson() && unique.params[1] == "Great Person")
|| unit.matchesFilter(unique.params[1])
if (unit.matchesFilter(unique.params[1])
) {
recipient.getDiplomacyManager(unit.civInfo).addInfluence(unique.params[0].toFloat() - 5f)
break