Allow for replacement improvements (#11369)

* Allow for replacement improvements

* imports

* Forgot the most important change, lol

* Docs

* Replacement description, validation, and filter

* Move more into ImprovementDescriptions

* Whoops, forgot to yield

* Fix some copy-paste artifacts

* New translations

* Fix double see also

* Add space for translation engine
This commit is contained in:
SeventhM 2024-04-04 13:39:44 -07:00 committed by GitHub
parent 0caf0cb4fa
commit 3ea1e4a539
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 264 additions and 139 deletions

View File

@ -1538,6 +1538,7 @@ Unique to [civName], replaces [unitName] =
Unique to [civName] = Unique to [civName] =
Tutorials = Tutorials =
Cost = Cost =
Turns to build =
May contain [listOfResources] = May contain [listOfResources] =
May contain: = May contain: =
Can upgrade from [unit] = Can upgrade from [unit] =
@ -1557,6 +1558,7 @@ Improvements that provide this resource =
Buildings that require this resource worked near the city = Buildings that require this resource worked near the city =
Units that consume this resource = Units that consume this resource =
Can be built on = Can be built on =
Cannot be built on =
or [terrainType] = or [terrainType] =
Can be constructed by = Can be constructed by =
Can be created instantly by = Can be created instantly by =

View File

@ -275,7 +275,7 @@ object Automation {
): Boolean { ): Boolean {
if (construction !is Building) return true if (construction !is Building) return true
if (!construction.hasCreateOneImprovementUnique()) return true // redundant but faster??? if (!construction.hasCreateOneImprovementUnique()) return true // redundant but faster???
val improvement = construction.getImprovementToCreate(city.getRuleset()) ?: return true val improvement = construction.getImprovementToCreate(city.getRuleset(), civInfo) ?: return true
return city.getTiles().any { return city.getTiles().any {
it.improvementFunctions.canBuildImprovement(improvement, civInfo) it.improvementFunctions.canBuildImprovement(improvement, civInfo)
} }

View File

@ -655,7 +655,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
tile: Tile? = null tile: Tile? = null
): Boolean { ): Boolean {
// Support UniqueType.CreatesOneImprovement: it is active when getImprovementToCreate returns an improvement // Support UniqueType.CreatesOneImprovement: it is active when getImprovementToCreate returns an improvement
val improvementToPlace = (construction as? Building)?.getImprovementToCreate(city.getRuleset()) val improvementToPlace = (construction as? Building)?.getImprovementToCreate(city.getRuleset(), city.civ)
if (improvementToPlace != null) { if (improvementToPlace != null) {
// If active without a predetermined tile to place the improvement on, automate a tile // If active without a predetermined tile to place the improvement on, automate a tile
val finalTile = tile val finalTile = tile
@ -716,7 +716,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
/** Support for [UniqueType.CreatesOneImprovement] - if an Improvement-creating Building was auto-queued, auto-choose a tile: */ /** Support for [UniqueType.CreatesOneImprovement] - if an Improvement-creating Building was auto-queued, auto-choose a tile: */
val building = getCurrentConstruction() as? Building ?: return val building = getCurrentConstruction() as? Building ?: return
val improvement = building.getImprovementToCreate(city.getRuleset()) ?: return val improvement = building.getImprovementToCreate(city.getRuleset(), city.civ) ?: return
if (getTileForImprovement(improvement.name) != null) return if (getTileForImprovement(improvement.name) != null) return
val newTile = Automation.getTileForConstructionImprovement(city, improvement) ?: return val newTile = Automation.getTileForConstructionImprovement(city, improvement) ?: return
newTile.improvementFunctions.markForCreatesOneImprovement(improvement.name) newTile.improvementFunctions.markForCreatesOneImprovement(improvement.name)
@ -778,7 +778,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
// UniqueType.CreatesOneImprovement support // UniqueType.CreatesOneImprovement support
val construction = getConstruction(constructionName) val construction = getConstruction(constructionName)
if (construction is Building) { if (construction is Building) {
val improvement = construction.getImprovementToCreate(city.getRuleset()) val improvement = construction.getImprovementToCreate(city.getRuleset(), city.civ)
if (improvement != null) { if (improvement != null) {
getTileForImprovement(improvement.name)?.stopWorkingOnImprovement() getTileForImprovement(improvement.name)?.stopWorkingOnImprovement()
} }
@ -846,7 +846,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
* (skip if none found), then un-mark the tile and place the improvement unless [removeOnly] is set. * (skip if none found), then un-mark the tile and place the improvement unless [removeOnly] is set.
*/ */
private fun applyCreateOneImprovement(building: Building, removeOnly: Boolean = false) { private fun applyCreateOneImprovement(building: Building, removeOnly: Boolean = false) {
val improvement = building.getImprovementToCreate(city.getRuleset()) val improvement = building.getImprovementToCreate(city.getRuleset(), city.civ)
?: return ?: return
val tileForImprovement = getTileForImprovement(improvement.name) ?: return val tileForImprovement = getTileForImprovement(improvement.name) ?: return
tileForImprovement.stopWorkingOnImprovement() // clears mark tileForImprovement.stopWorkingOnImprovement() // clears mark
@ -866,7 +866,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
val ruleset = city.getRuleset() val ruleset = city.getRuleset()
val indexToRemove = constructionQueue.withIndex().firstNotNullOfOrNull { val indexToRemove = constructionQueue.withIndex().firstNotNullOfOrNull {
val construction = getConstruction(it.value) val construction = getConstruction(it.value)
val buildingImprovement = (construction as? Building)?.getImprovementToCreate(ruleset)?.name val buildingImprovement = (construction as? Building)?.getImprovementToCreate(ruleset, city.civ)?.name
it.index.takeIf { buildingImprovement == improvement } it.index.takeIf { buildingImprovement == improvement }
} ?: return } ?: return

View File

@ -45,6 +45,7 @@ import com.unciv.models.ruleset.nation.Personality
import com.unciv.models.ruleset.tech.Era import com.unciv.models.ruleset.tech.Era
import com.unciv.models.ruleset.tile.ResourceSupplyList import com.unciv.models.ruleset.tile.ResourceSupplyList
import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.TileImprovement
import com.unciv.models.ruleset.tile.TileResource import com.unciv.models.ruleset.tile.TileResource
import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.TemporaryUnique import com.unciv.models.ruleset.unique.TemporaryUnique
@ -544,6 +545,22 @@ class Civilization : IsPartOfGameInfoSerialization {
return baseBuilding return baseBuilding
} }
fun getEquivalentTileImprovement(tileImprovementName: String): TileImprovement {
val tileImprovement = gameInfo.ruleset.tileImprovements[tileImprovementName]
?: throw UncivShowableException("Improvement $tileImprovementName doesn't seem to exist!")
return getEquivalentTileImprovement(tileImprovement)
}
fun getEquivalentTileImprovement(tileImprovement: TileImprovement): TileImprovement {
if (tileImprovement.replaces != null)
return getEquivalentTileImprovement(tileImprovement.replaces!!)
for (improvement in cache.uniqueImprovements)
if (improvement.replaces == tileImprovement.name)
return improvement
return tileImprovement
}
fun getEquivalentUnit(baseUnitName: String): BaseUnit { fun getEquivalentUnit(baseUnitName: String): BaseUnit {
val baseUnit = gameInfo.ruleset.units[baseUnitName] val baseUnit = gameInfo.ruleset.units[baseUnitName]
?: throw UncivShowableException("Unit $baseUnitName doesn't seem to exist!") ?: throw UncivShowableException("Unit $baseUnitName doesn't seem to exist!")

View File

@ -14,6 +14,7 @@ import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.tile.ResourceSupplyList import com.unciv.models.ruleset.tile.ResourceSupplyList
import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.TileImprovement
import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.models.ruleset.unique.UniqueTarget
import com.unciv.models.ruleset.unique.UniqueTriggerActivation import com.unciv.models.ruleset.unique.UniqueTriggerActivation
@ -35,6 +36,9 @@ class CivInfoTransientCache(val civInfo: Civilization) {
@Transient @Transient
val uniqueUnits = hashSetOf<BaseUnit>() val uniqueUnits = hashSetOf<BaseUnit>()
@Transient
val uniqueImprovements = hashSetOf<TileImprovement>()
@Transient @Transient
val uniqueBuildings = hashSetOf<Building>() val uniqueBuildings = hashSetOf<Building>()
@ -64,6 +68,10 @@ class CivInfoTransientCache(val civInfo: Civilization) {
} }
} }
for (improvement in ruleset.tileImprovements.values)
if (improvement.uniqueTo == civInfo.civName)
uniqueImprovements.add(improvement)
for (unit in ruleset.units.values) { for (unit in ruleset.units.values) {
if (unit.uniqueTo == civInfo.civName) { if (unit.uniqueTo == civInfo.civName) {
uniqueUnits.add(unit) uniqueUnits.add(unit)

View File

@ -20,6 +20,7 @@ enum class ImprovementBuildingProblem(
/** `true` if the ImprovementPicker should report this problem */ /** `true` if the ImprovementPicker should report this problem */
val reportable: Boolean = false val reportable: Boolean = false
) { ) {
Replaced(permanent = true),
WrongCiv(permanent = true), WrongCiv(permanent = true),
MissingTech(reportable = true), MissingTech(reportable = true),
Unbuildable(permanent = true), Unbuildable(permanent = true),
@ -45,6 +46,8 @@ class TileInfoImprovementFunctions(val tile: Tile) {
if (improvement.uniqueTo != null && improvement.uniqueTo != civInfo.civName) if (improvement.uniqueTo != null && improvement.uniqueTo != civInfo.civName)
yield(ImprovementBuildingProblem.WrongCiv) yield(ImprovementBuildingProblem.WrongCiv)
if (civInfo.cache.uniqueImprovements.any { it.replaces == improvement.name })
yield(ImprovementBuildingProblem.Replaced)
if (improvement.techRequired != null && !civInfo.tech.isResearched(improvement.techRequired!!)) if (improvement.techRequired != null && !civInfo.tech.isResearched(improvement.techRequired!!))
yield(ImprovementBuildingProblem.MissingTech) yield(ImprovementBuildingProblem.MissingTech)
if (improvement.getMatchingUniques(UniqueType.Unbuildable, StateForConditionals.IgnoreConditionals) if (improvement.getMatchingUniques(UniqueType.Unbuildable, StateForConditionals.IgnoreConditionals)

View File

@ -551,6 +551,10 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
} }
return _getImprovementToCreate return _getImprovementToCreate
} }
fun getImprovementToCreate(ruleset: Ruleset, civInfo: Civilization): TileImprovement? {
val improvement = getImprovementToCreate(ruleset) ?: return null
return civInfo.getEquivalentTileImprovement(improvement)
}
fun isSellable() = !isAnyWonder() && !hasUnique(UniqueType.Unsellable) fun isSellable() = !isAnyWonder() && !hasUnique(UniqueType.Unsellable)

View File

@ -13,6 +13,7 @@ import com.unciv.models.translations.tr
import com.unciv.ui.components.extensions.colorFromRGB import com.unciv.ui.components.extensions.colorFromRGB
import com.unciv.ui.objectdescriptions.BaseUnitDescriptions import com.unciv.ui.objectdescriptions.BaseUnitDescriptions
import com.unciv.ui.objectdescriptions.BuildingDescriptions import com.unciv.ui.objectdescriptions.BuildingDescriptions
import com.unciv.ui.objectdescriptions.ImprovementDescriptions
import com.unciv.ui.objectdescriptions.uniquesToCivilopediaTextLines import com.unciv.ui.objectdescriptions.uniquesToCivilopediaTextLines
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen.Companion.showReligionInCivilopedia import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen.Companion.showReligionInCivilopedia
import com.unciv.ui.screens.civilopediascreen.FormattedLine import com.unciv.ui.screens.civilopediascreen.FormattedLine
@ -246,17 +247,15 @@ class Nation : RulesetObject() {
yield(FormattedLine(separator = true)) yield(FormattedLine(separator = true))
yield(FormattedLine(improvement.name, link = "Improvement/${improvement.name}")) yield(FormattedLine(improvement.name, link = "Improvement/${improvement.name}"))
yield(FormattedLine(improvement.cloneStats().toString(), indent = 1)) // = (improvement as Stats).toString minus import plus copy overhead yield(FormattedLine(improvement.cloneStats().toString(), indent = 1)) // = (improvement as Stats).toString minus import plus copy overhead
if (improvement.terrainsCanBeBuiltOn.isNotEmpty()) { if (improvement.replaces != null && ruleset.tileImprovements.containsKey(improvement.replaces!!)) {
improvement.terrainsCanBeBuiltOn.withIndex().forEach { val originalImprovement = ruleset.tileImprovements[improvement.replaces!!]!!
yield( yield(FormattedLine("Replaces [${originalImprovement.name}]", link = originalImprovement.makeLink(), indent=1))
FormattedLine(if (it.index == 0) "{Can be built on} {${it.value}}" else "or [${it.value}]", yieldAll(ImprovementDescriptions.getDifferences(ruleset, originalImprovement, improvement))
link = "Terrain/${it.value}", indent = if (it.index == 0) 1 else 2) yield(FormattedLine())
) } else if (improvement.replaces != null) {
} yield(FormattedLine("Replaces [${improvement.replaces}], which is not found in the ruleset!", indent=1))
} } else {
for (unique in improvement.uniqueObjects) { yieldAll(improvement.getShortDecription())
if (unique.isHiddenToUsers()) continue
yield(FormattedLine(unique, indent = 1))
} }
} }
} }

View File

@ -1,28 +1,24 @@
package com.unciv.models.ruleset.tile package com.unciv.models.ruleset.tile
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.MultiFilter import com.unciv.logic.MultiFilter
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.RoadStatus import com.unciv.logic.map.tile.RoadStatus
import com.unciv.models.ruleset.Belief
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetStatsObject import com.unciv.models.ruleset.RulesetStatsObject
import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.models.ruleset.unique.UniqueTarget
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.translations.tr
import com.unciv.ui.components.extensions.toPercent import com.unciv.ui.components.extensions.toPercent
import com.unciv.ui.objectdescriptions.uniquesToCivilopediaTextLines import com.unciv.ui.objectdescriptions.ImprovementDescriptions
import com.unciv.ui.objectdescriptions.uniquesToDescription
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen.Companion.showReligionInCivilopedia
import com.unciv.ui.screens.civilopediascreen.FormattedLine import com.unciv.ui.screens.civilopediascreen.FormattedLine
import kotlin.math.roundToInt import kotlin.math.roundToInt
class TileImprovement : RulesetStatsObject() { class TileImprovement : RulesetStatsObject() {
var replaces: String? = null
var terrainsCanBeBuiltOn: Collection<String> = ArrayList() var terrainsCanBeBuiltOn: Collection<String> = ArrayList()
var techRequired: String? = null var techRequired: String? = null
var uniqueTo: String? = null var uniqueTo: String? = null
@ -44,29 +40,8 @@ class TileImprovement : RulesetStatsObject() {
// In some weird cases it was possible for something to take 0 turns, leading to it instead never finishing // In some weird cases it was possible for something to take 0 turns, leading to it instead never finishing
} }
fun getDescription(ruleset: Ruleset): String { fun getDescription(ruleset: Ruleset): String = ImprovementDescriptions.getDescription(this, ruleset)
val lines = ArrayList<String>() fun getShortDecription() = ImprovementDescriptions.getShortDescription(this)
val statsDesc = cloneStats().toString()
if (statsDesc.isNotEmpty()) lines += statsDesc
if (!terrainsCanBeBuiltOn.isEmpty()) {
val terrainsCanBeBuiltOnString: ArrayList<String> = arrayListOf()
for (i in terrainsCanBeBuiltOn) {
terrainsCanBeBuiltOnString.add(i.tr())
}
lines += "Can be built on".tr() + terrainsCanBeBuiltOnString.joinToString(", ", " ") //language can be changed when setting changes.
}
for (resource: TileResource in ruleset.tileResources.values.filter { it.isImprovedBy(name) }) {
if (resource.improvementStats == null) continue
val statsString = resource.improvementStats.toString()
lines += "[${statsString}] <in [${resource.name}] tiles>".tr()
}
if (techRequired != null) lines += "Required tech: [$techRequired]".tr()
uniquesToDescription(lines)
return lines.joinToString("\n")
}
fun isGreatImprovement() = hasUnique(UniqueType.GreatImprovement) fun isGreatImprovement() = hasUnique(UniqueType.GreatImprovement)
fun isRoad() = RoadStatus.values().any { it != RoadStatus.None && it.name == this.name } fun isRoad() = RoadStatus.values().any { it != RoadStatus.None && it.name == this.name }
@ -100,6 +75,7 @@ class TileImprovement : RulesetStatsObject() {
private fun matchesSingleFilter(filter: String): Boolean { private fun matchesSingleFilter(filter: String): Boolean {
return when (filter) { return when (filter) {
name -> true name -> true
replaces -> true
in Constants.all -> true in Constants.all -> true
"Improvement" -> true // For situations involving tileFilter "Improvement" -> true // For situations involving tileFilter
"All Road" -> isRoad() "All Road" -> isRoad()
@ -111,95 +87,10 @@ class TileImprovement : RulesetStatsObject() {
override fun makeLink() = "Improvement/$name" override fun makeLink() = "Improvement/$name"
override fun getCivilopediaTextLines(ruleset: Ruleset): List<FormattedLine> { override fun getCivilopediaTextLines(ruleset: Ruleset): List<FormattedLine> =
val textList = ArrayList<FormattedLine>() ImprovementDescriptions.getCivilopediaTextLines(this, ruleset)
val statsDesc = cloneStats().toString() fun getConstructorUnits(ruleset: Ruleset): List<BaseUnit> {
if (statsDesc.isNotEmpty()) textList += FormattedLine(statsDesc)
if (uniqueTo != null) {
textList += FormattedLine()
textList += FormattedLine("Unique to [$uniqueTo]", link="Nation/$uniqueTo")
}
val constructorUnits = getConstructorUnits(ruleset)
val creatingUnits = getCreatingUnits(ruleset)
val creatorExists = constructorUnits.isNotEmpty() || creatingUnits.isNotEmpty()
if (creatorExists && terrainsCanBeBuiltOn.isNotEmpty()) {
textList += FormattedLine()
if (terrainsCanBeBuiltOn.size == 1) {
with (terrainsCanBeBuiltOn.first()) {
textList += FormattedLine("{Can be built on} {$this}", link="Terrain/$this")
}
} else {
textList += FormattedLine("{Can be built on}:")
terrainsCanBeBuiltOn.forEach {
textList += FormattedLine(it, link="Terrain/$it", indent=1)
}
}
}
var addedLineBeforeResourceBonus = false
for (resource in ruleset.tileResources.values) {
if (resource.improvementStats == null || !resource.isImprovedBy(name)) continue
if (!addedLineBeforeResourceBonus) {
addedLineBeforeResourceBonus = true
textList += FormattedLine()
}
val statsString = resource.improvementStats.toString()
// Line intentionally modeled as UniqueType.Stats + ConditionalInTiles
textList += FormattedLine("[${statsString}] <in [${resource.name}] tiles>", link = resource.makeLink())
}
if (techRequired != null) {
textList += FormattedLine()
textList += FormattedLine("Required tech: [$techRequired]", link="Technology/$techRequired")
}
uniquesToCivilopediaTextLines(textList)
// Be clearer when one needs to chop down a Forest first... A "Can be built on Plains" is clear enough,
// but a "Can be built on Land" is not - how is the user to know Forest is _not_ Land?
if (creatorExists &&
!isEmpty() && // Has any Stats
!hasUnique(UniqueType.NoFeatureRemovalNeeded) &&
!hasUnique(UniqueType.RemovesFeaturesIfBuilt) &&
terrainsCanBeBuiltOn.none { it in ruleset.terrains }
)
textList += FormattedLine("Needs removal of terrain features to be built")
if (isAncientRuinsEquivalent() && ruleset.ruinRewards.isNotEmpty()) {
val difficulty = if (!UncivGame.isCurrentInitialized() || UncivGame.Current.gameInfo == null)
"Prince" // most factors == 1
else UncivGame.Current.gameInfo!!.gameParameters.difficulty
val religionEnabled = showReligionInCivilopedia(ruleset)
textList += FormattedLine()
textList += FormattedLine("The possible rewards are:")
ruleset.ruinRewards.values.asSequence()
.filter { reward ->
difficulty !in reward.excludedDifficulties &&
(religionEnabled || !reward.hasUnique(UniqueType.HiddenWithoutReligion))
}
.forEach { reward ->
textList += FormattedLine(reward.name, starred = true, color = reward.color)
textList += reward.civilopediaText
}
}
if (creatorExists)
textList += FormattedLine()
for (unit in constructorUnits)
textList += FormattedLine("{Can be constructed by} {$unit}", unit.makeLink())
for (unit in creatingUnits)
textList += FormattedLine("{Can be created instantly by} {$unit}", unit.makeLink())
textList += Belief.getCivilopediaTextMatching(name, ruleset)
return textList
}
private fun getConstructorUnits(ruleset: Ruleset): List<BaseUnit> {
//todo Why does this have to be so complicated? A unit's "Can build [Land] improvements on tiles" //todo Why does this have to be so complicated? A unit's "Can build [Land] improvements on tiles"
// creates the _justified_ expectation that an improvement it can build _will_ have // creates the _justified_ expectation that an improvement it can build _will_ have
// `matchesFilter("Land")==true` - but that's not the case. // `matchesFilter("Land")==true` - but that's not the case.
@ -251,7 +142,7 @@ class TileImprovement : RulesetStatsObject() {
}.toList() }.toList()
} }
private fun getCreatingUnits(ruleset: Ruleset): List<BaseUnit> { fun getCreatingUnits(ruleset: Ruleset): List<BaseUnit> {
return ruleset.units.values.asSequence() return ruleset.units.values.asSequence()
.filter { unit -> .filter { unit ->
unit.getMatchingUniques(UniqueType.ConstructImprovementInstantly, StateForConditionals.IgnoreConditionals) unit.getMatchingUniques(UniqueType.ConstructImprovementInstantly, StateForConditionals.IgnoreConditionals)

View File

@ -401,13 +401,15 @@ class RulesetValidator(val ruleset: Ruleset) {
for (improvement in ruleset.tileImprovements.values) { for (improvement in ruleset.tileImprovements.values) {
if (improvement.techRequired != null && !ruleset.technologies.containsKey(improvement.techRequired!!)) if (improvement.techRequired != null && !ruleset.technologies.containsKey(improvement.techRequired!!))
lines.add("${improvement.name} requires tech ${improvement.techRequired} which does not exist!", sourceObject = improvement) lines.add("${improvement.name} requires tech ${improvement.techRequired} which does not exist!", sourceObject = improvement)
if (improvement.replaces != null && !ruleset.tileImprovements.containsKey(improvement.replaces))
lines.add("${improvement.name} replaces ${improvement.replaces} which does not exist!", sourceObject = improvement)
for (terrain in improvement.terrainsCanBeBuiltOn) for (terrain in improvement.terrainsCanBeBuiltOn)
if (!ruleset.terrains.containsKey(terrain) && terrain != "Land" && terrain != "Water") if (!ruleset.terrains.containsKey(terrain) && terrain != "Land" && terrain != "Water")
lines.add("${improvement.name} can be built on terrain $terrain which does not exist!", sourceObject = improvement) lines.add("${improvement.name} can be built on terrain $terrain which does not exist!", sourceObject = improvement)
if (improvement.terrainsCanBeBuiltOn.isEmpty() if (improvement.terrainsCanBeBuiltOn.isEmpty()
&& !improvement.hasUnique(UniqueType.CanOnlyImproveResource) && !improvement.hasUnique(UniqueType.CanOnlyImproveResource)
&& !improvement.hasUnique(UniqueType.Unbuildable) && !improvement.hasUnique(UniqueType.Unbuildable)
&& !improvement.name.startsWith(Constants.remove) && improvement !in ruleset.tileRemovals
&& improvement.name !in RoadStatus.values().map { it.removeAction } && improvement.name !in RoadStatus.values().map { it.removeAction }
&& improvement.name != Constants.cancelImprovementOrder && improvement.name != Constants.cancelImprovementOrder
) { ) {

View File

@ -0,0 +1,197 @@
package com.unciv.ui.objectdescriptions
import com.unciv.UncivGame
import com.unciv.models.ruleset.Belief
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.tile.TileImprovement
import com.unciv.models.ruleset.tile.TileResource
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.tr
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen
import com.unciv.ui.screens.civilopediascreen.FormattedLine
object ImprovementDescriptions {
/**
* Lists differences: how a nation-unique Improvement compares to its replacement.
*
* Result as indented, non-linking [FormattedLine]s
*
* @param originalImprovement The "standard" Improvement
* @param replacementImprovement The "uniqueTo" Improvement
*/
fun getDifferences(
ruleset: Ruleset, originalImprovement: TileImprovement, replacementImprovement: TileImprovement
): Sequence<FormattedLine> = sequence {
for ((key, value) in replacementImprovement)
if (value != originalImprovement[key])
yield(FormattedLine( key.name.tr() + " " +"[${value.toInt()}] vs [${originalImprovement[key].toInt()}]".tr(), indent=1))
for (terrain in replacementImprovement.terrainsCanBeBuiltOn)
if (terrain !in originalImprovement.terrainsCanBeBuiltOn)
yield(FormattedLine("Can be built on [${terrain}]", link = ruleset.terrains[terrain]?.makeLink() ?: "", indent = 1))
for (terrain in originalImprovement.terrainsCanBeBuiltOn)
if (terrain !in replacementImprovement.terrainsCanBeBuiltOn)
yield(FormattedLine("Cannot be built on [${terrain}]", link = ruleset.terrains[terrain]?.makeLink() ?: "", indent = 1))
if (replacementImprovement.turnsToBuild != originalImprovement.turnsToBuild)
yield(FormattedLine("{Turns to build} ".tr() + "[${replacementImprovement.turnsToBuild}] vs [${originalImprovement.turnsToBuild}]".tr(), indent=1))
val newAbilityPredicate: (Unique)->Boolean = { it.text in originalImprovement.uniques || it.isHiddenToUsers() }
for (unique in replacementImprovement.uniqueObjects.filterNot(newAbilityPredicate))
yield(FormattedLine(unique.text, indent=1)) // FormattedLine(unique) would look worse - no indent and autolinking could distract
val lostAbilityPredicate: (Unique)->Boolean = { it.text in replacementImprovement.uniques || it.isHiddenToUsers() }
for (unique in originalImprovement.uniqueObjects.filterNot(lostAbilityPredicate)) {
// Need double translation of the "ability" here - unique texts may contain square brackets
yield(FormattedLine("Lost ability (vs [${originalImprovement.name}]): [${unique.text.tr()}]", indent=1))
}
}
fun getCivilopediaTextLines(improvement: TileImprovement, ruleset: Ruleset): List<FormattedLine> {
val textList = ArrayList<FormattedLine>()
val statsDesc = improvement.cloneStats().toString()
if (statsDesc.isNotEmpty()) textList += FormattedLine(statsDesc)
if (improvement.uniqueTo != null) {
textList += FormattedLine()
textList += FormattedLine("Unique to [${improvement.uniqueTo}]", link="Nation/${improvement.uniqueTo}")
}
if (improvement.replaces != null) {
val replaceImprovement = ruleset.tileImprovements[improvement.replaces]
textList += FormattedLine("Replaces [${improvement.replaces}]", link=replaceImprovement?.makeLink() ?: "", indent = 1)
}
val constructorUnits = improvement.getConstructorUnits(ruleset)
val creatingUnits = improvement.getCreatingUnits(ruleset)
val creatorExists = constructorUnits.isNotEmpty() || creatingUnits.isNotEmpty()
if (creatorExists && improvement.terrainsCanBeBuiltOn.isNotEmpty()) {
textList += FormattedLine()
if (improvement.terrainsCanBeBuiltOn.size == 1) {
with (improvement.terrainsCanBeBuiltOn.first()) {
textList += FormattedLine("{Can be built on} {$this}", link="Terrain/$this")
}
} else {
textList += FormattedLine("{Can be built on}:")
improvement.terrainsCanBeBuiltOn.forEach {
textList += FormattedLine(it, link="Terrain/$it", indent=1)
}
}
}
var addedLineBeforeResourceBonus = false
for (resource in ruleset.tileResources.values) {
if (resource.improvementStats == null || !resource.isImprovedBy(improvement.name)) continue
if (!addedLineBeforeResourceBonus) {
addedLineBeforeResourceBonus = true
textList += FormattedLine()
}
val statsString = resource.improvementStats.toString()
// Line intentionally modeled as UniqueType.Stats + ConditionalInTiles
textList += FormattedLine("[${statsString}] <in [${resource.name}] tiles>", link = resource.makeLink())
}
if (improvement.techRequired != null) {
textList += FormattedLine()
textList += FormattedLine("Required tech: [${improvement.techRequired}]", link="Technology/${improvement.techRequired}")
}
improvement.uniquesToCivilopediaTextLines(textList)
// Be clearer when one needs to chop down a Forest first... A "Can be built on Plains" is clear enough,
// but a "Can be built on Land" is not - how is the user to know Forest is _not_ Land?
if (creatorExists &&
!improvement.isEmpty() && // Has any Stats
!improvement.hasUnique(UniqueType.NoFeatureRemovalNeeded) &&
!improvement.hasUnique(UniqueType.RemovesFeaturesIfBuilt) &&
improvement.terrainsCanBeBuiltOn.none { it in ruleset.terrains }
)
textList += FormattedLine("Needs removal of terrain features to be built")
if (improvement.isAncientRuinsEquivalent() && ruleset.ruinRewards.isNotEmpty()) {
val difficulty = if (!UncivGame.isCurrentInitialized() || UncivGame.Current.gameInfo == null)
"Prince" // most factors == 1
else UncivGame.Current.gameInfo!!.gameParameters.difficulty
val religionEnabled = CivilopediaScreen.showReligionInCivilopedia(ruleset)
textList += FormattedLine()
textList += FormattedLine("The possible rewards are:")
ruleset.ruinRewards.values.asSequence()
.filter { reward ->
difficulty !in reward.excludedDifficulties &&
(religionEnabled || !reward.hasUnique(UniqueType.HiddenWithoutReligion))
}
.forEach { reward ->
textList += FormattedLine(reward.name, starred = true, color = reward.color)
textList += reward.civilopediaText
}
}
if (creatorExists)
textList += FormattedLine()
for (unit in constructorUnits)
textList += FormattedLine("{Can be constructed by} {$unit}", unit.makeLink())
for (unit in creatingUnits)
textList += FormattedLine("{Can be created instantly by} {$unit}", unit.makeLink())
val seeAlso = ArrayList<FormattedLine>()
for (alsoImprovement in ruleset.tileImprovements.values) {
if (alsoImprovement.replaces == improvement.name)
seeAlso += FormattedLine(alsoImprovement.name, link = alsoImprovement.makeLink(), indent = 1)
}
seeAlso += Belief.getCivilopediaTextMatching(improvement.name, ruleset, false)
if (seeAlso.isNotEmpty()) {
textList += FormattedLine()
textList += FormattedLine("{See also}:")
textList += seeAlso
}
return textList
}
fun getDescription(improvement: TileImprovement, ruleset: Ruleset): String {
val lines = ArrayList<String>()
val statsDesc = improvement.cloneStats().toString()
if (statsDesc.isNotEmpty()) lines += statsDesc
if (improvement.uniqueTo != null) lines += "Unique to [${improvement.uniqueTo}]".tr()
if (improvement.replaces != null) lines += "Replaces [${improvement.replaces}]".tr()
if (!improvement.terrainsCanBeBuiltOn.isEmpty()) {
val terrainsCanBeBuiltOnString: ArrayList<String> = arrayListOf()
for (i in improvement.terrainsCanBeBuiltOn) {
terrainsCanBeBuiltOnString.add(i.tr())
}
lines += "Can be built on".tr() + terrainsCanBeBuiltOnString.joinToString(", ", " ") //language can be changed when setting changes.
}
for (resource: TileResource in ruleset.tileResources.values.filter { it.isImprovedBy(improvement.name) }) {
if (resource.improvementStats == null) continue
val statsString = resource.improvementStats.toString()
lines += "[${statsString}] <in [${resource.name}] tiles>".tr()
}
if (improvement.techRequired != null) lines += "Required tech: [${improvement.techRequired}]".tr()
improvement.uniquesToDescription(lines)
return lines.joinToString("\n")
}
fun getShortDescription(improvement: TileImprovement) = sequence {
if (improvement.terrainsCanBeBuiltOn.isNotEmpty()) {
improvement.terrainsCanBeBuiltOn.withIndex().forEach {
yield(
FormattedLine(
if (it.index == 0) "{Can be built on} {${it.value}}" else "or [${it.value}]",
link = "Terrain/${it.value}", indent = if (it.index == 0) 1 else 2
)
)
}
}
for (unique in improvement.uniqueObjects) {
if (unique.isHiddenToUsers()) continue
yield(FormattedLine(unique, indent = 1))
}
}
}

View File

@ -660,7 +660,8 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
return cityScreen.startPickTileForCreatesOneImprovement(construction, stat, true) return cityScreen.startPickTileForCreatesOneImprovement(construction, stat, true)
// Buying a UniqueType.CreatesOneImprovement building from queue must pass down // Buying a UniqueType.CreatesOneImprovement building from queue must pass down
// the already selected tile, otherwise a new one is chosen from Automation code. // the already selected tile, otherwise a new one is chosen from Automation code.
val improvement = construction.getImprovementToCreate(cityScreen.city.getRuleset())!! val improvement = construction.getImprovementToCreate(
cityScreen.city.getRuleset(), cityScreen.city.civ)!!
val tileForImprovement = cityScreen.city.cityConstructions.getTileForImprovement(improvement.name) val tileForImprovement = cityScreen.city.cityConstructions.getTileForImprovement(improvement.name)
askToBuyConstruction(construction, stat, tileForImprovement) askToBuyConstruction(construction, stat, tileForImprovement)
} }

View File

@ -454,7 +454,7 @@ class CityScreen(
fun selectConstruction(newConstruction: IConstruction) { fun selectConstruction(newConstruction: IConstruction) {
selectedConstruction = newConstruction selectedConstruction = newConstruction
if (newConstruction is Building && newConstruction.hasCreateOneImprovementUnique()) { if (newConstruction is Building && newConstruction.hasCreateOneImprovementUnique()) {
val improvement = newConstruction.getImprovementToCreate(city.getRuleset()) val improvement = newConstruction.getImprovementToCreate(city.getRuleset(), city.civ)
selectedQueueEntryTargetTile = if (improvement == null) null selectedQueueEntryTargetTile = if (improvement == null) null
else city.cityConstructions.getTileForImprovement(improvement.name) else city.cityConstructions.getTileForImprovement(improvement.name)
} else { } else {
@ -472,7 +472,7 @@ class CityScreen(
fun clearSelection() = selectTile(null) fun clearSelection() = selectTile(null)
fun startPickTileForCreatesOneImprovement(construction: Building, stat: Stat, isBuying: Boolean) { fun startPickTileForCreatesOneImprovement(construction: Building, stat: Stat, isBuying: Boolean) {
val improvement = construction.getImprovementToCreate(city.getRuleset()) ?: return val improvement = construction.getImprovementToCreate(city.getRuleset(), city.civ) ?: return
pickTileData = PickTileForImprovementData(construction, improvement, isBuying, stat) pickTileData = PickTileForImprovementData(construction, improvement, isBuying, stat)
updateTileGroups() updateTileGroups()
ToastPopup("Please select a tile for this building's [${improvement.name}]", this) ToastPopup("Please select a tile for this building's [${improvement.name}]", this)

View File

@ -45,6 +45,7 @@ Each improvement has the following structure:
| name | String | Required | [^A] | | name | String | Required | [^A] |
| terrainsCanBeBuiltOn | List of Strings | empty | Terrains that this improvement can be built on [^B]. Removable terrain features will need to be removed before building an improvement [^C]. Must be in [Terrains.json](#terrainsjson) | | terrainsCanBeBuiltOn | List of Strings | empty | Terrains that this improvement can be built on [^B]. Removable terrain features will need to be removed before building an improvement [^C]. Must be in [Terrains.json](#terrainsjson) |
| techRequired | String | none | The name of the technology required to build this improvement | | techRequired | String | none | The name of the technology required to build this improvement |
| replaces | String | none | The name of a improvement that should be replaced by this improvement. Must be in [TileImprovements.json](#TileImprovementsjson) |
| uniqueTo | String | none | The name of the nation this improvement is unique for | | uniqueTo | String | none | The name of the nation this improvement is unique for |
| [`<stats>`](#stats) | Integer | 0 | Per-turn bonus yield for the tile | | [`<stats>`](#stats) | Integer | 0 | Per-turn bonus yield for the tile |
| turnsToBuild | Integer | -1 | Number of turns a worker spends building this. If -1, the improvement is unbuildable [^D]. If 0, the improvement is always built in one turn | | turnsToBuild | Integer | -1 | Number of turns a worker spends building this. If -1, the improvement is unbuildable [^D]. If 0, the improvement is always built in one turn |