From 60e2af3bba4c3fb6c3cfe0e1afeb410083a67392 Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Mon, 13 Nov 2023 21:19:47 +0100 Subject: [PATCH] Fix "hidden from users" where Nation describes its unique Building (if it replaces an existing one) (#10467) * Refactor: move UI functions from Building to BuildingDescriptions * Move Nation "unique building" diff code to BuildingDescriptions, fix hidden ignore, equalize to how BaseUnitDescriptions.getDifferences works * Fix translation errors for difference description "Lost ability" --- core/src/com/unciv/models/ruleset/Building.kt | 236 +-------------- .../com/unciv/models/ruleset/nation/Nation.kt | 19 +- .../BaseUnitDescriptions.kt | 3 +- .../BuildingDescriptions.kt | 281 ++++++++++++++++++ 4 files changed, 295 insertions(+), 244 deletions(-) create mode 100644 core/src/com/unciv/ui/objectdescriptions/BuildingDescriptions.kt diff --git a/core/src/com/unciv/models/ruleset/Building.kt b/core/src/com/unciv/models/ruleset/Building.kt index c6028849d2..9393d02f67 100644 --- a/core/src/com/unciv/models/ruleset/Building.kt +++ b/core/src/com/unciv/models/ruleset/Building.kt @@ -8,20 +8,15 @@ import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.tile.TileImprovement import com.unciv.models.ruleset.unique.LocalUniqueCache import com.unciv.models.ruleset.unique.StateForConditionals -import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueParameterType import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.stats.Stat import com.unciv.models.stats.Stats import com.unciv.models.translations.fillPlaceholders -import com.unciv.models.translations.tr -import com.unciv.ui.components.extensions.getConsumesAmountString import com.unciv.ui.components.extensions.getNeedMoreAmountString import com.unciv.ui.components.extensions.toPercent -import com.unciv.ui.components.fonts.Fonts -import com.unciv.ui.objectdescriptions.uniquesToCivilopediaTextLines -import com.unciv.ui.screens.civilopediascreen.FormattedLine +import com.unciv.ui.objectdescriptions.BuildingDescriptions class Building : RulesetStatsObject(), INonPerpetualConstruction { @@ -44,130 +39,24 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction { var requiredBuilding: String? = null /** A strategic resource that will be consumed by this building */ - private var requiredResource: String? = null + var requiredResource: String? = null - /** City can only be built if one of these resources is nearby - it must be improved! */ + /** This Building can only be built if one of these resources is nearby - it must be improved! */ var requiredNearbyImprovedResources: List? = null var cityStrength = 0 var cityHealth = 0 var replaces: String? = null var uniqueTo: String? = null var quote: String = "" + var replacementTextForUniques = "" + override fun getUniqueTarget() = if (isAnyWonder()) UniqueTarget.Wonder else UniqueTarget.Building - private var replacementTextForUniques = "" - /** Used for AlertType.WonderBuilt, and as sub-text in Nation and Tech descriptions */ - fun getShortDescription(multiline:Boolean = false): String { - val infoList = mutableListOf() - this.clone().toString().also { if (it.isNotEmpty()) infoList += it } - for ((key, value) in getStatPercentageBonuses(null)) - infoList += "+${value.toInt()}% ${key.name.tr()}" + override fun makeLink() = if (isAnyWonder()) "Wonder/$name" else "Building/$name" - if (requiredNearbyImprovedResources != null) - infoList += "Requires worked [" + requiredNearbyImprovedResources!!.joinToString("/") { it.tr() } + "] near city" - if (uniques.isNotEmpty()) { - if (replacementTextForUniques != "") infoList += replacementTextForUniques - else infoList += getUniquesStringsWithoutDisablers() - } - if (cityStrength != 0) infoList += "{City strength} +$cityStrength" - if (cityHealth != 0) infoList += "{City health} +$cityHealth" - val separator = if (multiline) "\n" else "; " - return infoList.joinToString(separator) { it.tr() } - } - - /** - * @param filterUniques If provided, include only uniques for which this function returns true. - */ - private fun getUniquesStrings(filterUniques: ((Unique) -> Boolean)? = null) = sequence { - val tileBonusHashmap = HashMap>() - for (unique in uniqueObjects) if (filterUniques == null || filterUniques(unique)) when { - unique.isOfType(UniqueType.StatsFromTiles) && unique.params[2] == "in this city" -> { - val stats = unique.params[0] - if (!tileBonusHashmap.containsKey(stats)) tileBonusHashmap[stats] = ArrayList() - tileBonusHashmap[stats]!!.add(unique.params[1]) - } - else -> yield(unique.text) - } - for ((key, value) in tileBonusHashmap) - yield( "[stats] from [tileFilter] tiles in this city" - .fillPlaceholders( key, - // A single tileFilter will be properly translated later due to being within [] - // advantage to not translate prematurely: FormatLine.formatUnique will recognize it - if (value.size == 1) value[0] else value.joinToString { it.tr() } - )) - } - /** - * @param filterUniques If provided, include only uniques for which this function returns true. - */ - private fun getUniquesStringsWithoutDisablers(filterUniques: ((Unique) -> Boolean)? = null) = getUniquesStrings { - !it.isHiddenToUsers() - && filterUniques?.invoke(it) ?: true - } - - /** used in CityScreen (ConstructionInfoTable) */ - fun getDescription(city: City, showAdditionalInfo: Boolean): String { - val stats = getStats(city) - val translatedLines = ArrayList() // Some translations require special handling - val isFree = city.civ.civConstructions.hasFreeBuilding(city, this) - if (uniqueTo != null) translatedLines += if (replaces == null) "Unique to [$uniqueTo]".tr() - else "Unique to [$uniqueTo], replaces [$replaces]".tr() - val missingUnique = getMatchingUniques(UniqueType.RequiresBuildingInAllCities).firstOrNull() - if (isWonder) translatedLines += "Wonder".tr() - if (isNationalWonder) translatedLines += "National Wonder".tr() - if (!isFree) { - for ((resourceName, amount) in getResourceRequirementsPerTurn(StateForConditionals(city.civ, city))) { - val available = city.getResourceAmount(resourceName) - val resource = city.getRuleset().tileResources[resourceName] ?: continue - val consumesString = resourceName.getConsumesAmountString(amount, resource.isStockpiled()) - - translatedLines += if (showAdditionalInfo) "$consumesString ({[$available] available})".tr() - else consumesString.tr() - } - } - - // Inefficient in theory. In practice, buildings seem to have only a small handful of uniques. - val missingCities = if (missingUnique != null) - // TODO: Unify with rejection reasons? - city.civ.cities.filterNot { - it.isPuppet - || it.cityConstructions.containsBuildingOrEquivalent(missingUnique.params[0]) - } - else listOf() - if (uniques.isNotEmpty()) { - if (replacementTextForUniques != "") translatedLines += replacementTextForUniques.tr() - else translatedLines += getUniquesStringsWithoutDisablers( - filterUniques = if (missingCities.isEmpty()) null - else { unique -> !unique.isOfType(UniqueType.RequiresBuildingInAllCities) } - // Filter out the "Requires a [] in all cities" unique if any cities are still missing the required building, since in that case the list of cities will be appended at the end. - ).map { it.tr() } - } - if (!stats.isEmpty()) - translatedLines += stats.toString() - - for ((stat, value) in getStatPercentageBonuses(city)) - if (value != 0f) translatedLines += "+${value.toInt()}% {${stat.name}}".tr() - - for ((greatPersonName, value) in greatPersonPoints) - translatedLines += "+$value " + "[$greatPersonName] points".tr() - - for ((specialistName, amount) in newSpecialists()) - translatedLines += "+$amount " + "[$specialistName] slots".tr() - - if (requiredNearbyImprovedResources != null) - translatedLines += "Requires worked [${requiredNearbyImprovedResources!!.joinToString("/") { it.tr() }}] near city".tr() - - if (cityStrength != 0) translatedLines += "{City strength} +$cityStrength".tr() - if (cityHealth != 0) translatedLines += "{City health} +$cityHealth".tr() - if (maintenance != 0 && !isFree) translatedLines += "{Maintenance cost}: $maintenance {Gold}".tr() - if (showAdditionalInfo && missingCities.isNotEmpty()) { - // Could be red. But IMO that should be done by enabling GDX's ColorMarkupLanguage globally instead of adding a separate label. - translatedLines += "\n" + - "[${city.civ.getEquivalentBuilding(missingUnique!!.params[0])}] required:".tr() + - " " + missingCities.joinToString(", ") { it.name.tr(hideIcons = true) } - // Can't nest square bracket placeholders inside curlies, and don't see any way to define wildcard placeholders. So run translation explicitly on base text. - } - return translatedLines.joinToString("\n").trim() - } + fun getShortDescription(multiline: Boolean = false) = BuildingDescriptions.getShortDescription(this, multiline) + fun getDescription(city: City, showAdditionalInfo: Boolean) = BuildingDescriptions.getDescription(this, city, showAdditionalInfo) + override fun getCivilopediaTextLines(ruleset: Ruleset) = BuildingDescriptions.getCivilopediaTextLines(this, ruleset) fun getStats(city: City, /* By default, do not cache - if we're getting stats for only one building this isn't efficient. @@ -211,113 +100,6 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction { return stats } - override fun makeLink() = if (isAnyWonder()) "Wonder/$name" else "Building/$name" - - override fun getCivilopediaTextLines(ruleset: Ruleset): List { - fun Float.formatSignedInt() = (if (this > 0f) "+" else "") + this.toInt().toString() - - val textList = ArrayList() - - if (isAnyWonder()) { - textList += FormattedLine( if (isWonder) "Wonder" else "National Wonder", color="#CA4", header=3 ) - } - - if (uniqueTo != null) { - textList += FormattedLine() - textList += FormattedLine("Unique to [$uniqueTo]", link="Nation/$uniqueTo") - if (replaces != null) { - val replacesBuilding = ruleset.buildings[replaces] - textList += FormattedLine("Replaces [$replaces]", link=replacesBuilding?.makeLink() ?: "", indent=1) - } - } - - if (cost > 0) { - val stats = mutableListOf("$cost${Fonts.production}") - if (canBePurchasedWithStat(null, Stat.Gold)) { - stats += "${getCivilopediaGoldCost()}${Fonts.gold}" - } - textList += FormattedLine(stats.joinToString("/", "{Cost}: ")) - } - - if (requiredTech != null) - textList += FormattedLine("Required tech: [$requiredTech]", - link="Technology/$requiredTech") - if (requiredBuilding != null) - textList += FormattedLine("Requires [$requiredBuilding] to be built in the city", - link="Building/$requiredBuilding") - - if (requiredResource != null) { - textList += FormattedLine() - val resource = ruleset.tileResources[requiredResource] - textList += FormattedLine( - requiredResource!!.getConsumesAmountString(1, resource!!.isStockpiled()), - link="Resources/$requiredResource", color="#F42" ) - } - - val stats = cloneStats() - val percentStats = getStatPercentageBonuses(null) - val specialists = newSpecialists() - if (uniques.isNotEmpty() || !stats.isEmpty() || !percentStats.isEmpty() || this.greatPersonPoints.isNotEmpty() || specialists.isNotEmpty()) - textList += FormattedLine() - - if (replacementTextForUniques.isNotEmpty()) { - textList += FormattedLine(replacementTextForUniques) - } else { - uniquesToCivilopediaTextLines(textList, colorConsumesResources = true) - } - - if (!stats.isEmpty()) { - textList += FormattedLine(stats.toString()) - } - - if (!percentStats.isEmpty()) { - for ((key, value) in percentStats) { - if (value == 0f) continue - textList += FormattedLine(value.formatSignedInt() + "% {$key}") - } - } - - for ((greatPersonName, value) in greatPersonPoints) { - textList += FormattedLine( - "+$value " + "[$greatPersonName] points".tr(), - link = "Unit/$greatPersonName" - ) - } - - if (specialists.isNotEmpty()) { - for ((specialistName, amount) in specialists) - textList += FormattedLine("+$amount " + "[$specialistName] slots".tr()) - } - - if (requiredNearbyImprovedResources != null) { - textList += FormattedLine() - textList += FormattedLine("Requires at least one of the following resources worked near the city:") - requiredNearbyImprovedResources!!.forEach { - textList += FormattedLine(it, indent = 1, link = "Resource/$it") - } - } - - if (cityStrength != 0 || cityHealth != 0 || maintenance != 0) textList += FormattedLine() - if (cityStrength != 0) textList += FormattedLine("{City strength} +$cityStrength") - if (cityHealth != 0) textList += FormattedLine("{City health} +$cityHealth") - if (maintenance != 0) textList += FormattedLine("{Maintenance cost}: $maintenance {Gold}") - - val seeAlso = ArrayList() - for (building in ruleset.buildings.values) { - if (building.replaces == name - || building.uniqueObjects.any { unique -> unique.params.any { it == name } }) - seeAlso += FormattedLine(building.name, link=building.makeLink(), indent=1) - } - seeAlso += Belief.getCivilopediaTextMatching(name, ruleset, false) - if (seeAlso.isNotEmpty()) { - textList += FormattedLine() - textList += FormattedLine("{See also}:") - textList += seeAlso - } - - return textList - } - override fun getProductionCost(civInfo: Civilization): Int { var productionCost = cost.toFloat() diff --git a/core/src/com/unciv/models/ruleset/nation/Nation.kt b/core/src/com/unciv/models/ruleset/nation/Nation.kt index 1e5ea3a9d1..178de9f76b 100644 --- a/core/src/com/unciv/models/ruleset/nation/Nation.kt +++ b/core/src/com/unciv/models/ruleset/nation/Nation.kt @@ -10,6 +10,7 @@ import com.unciv.models.translations.squareBraceRegex import com.unciv.models.translations.tr import com.unciv.ui.components.extensions.colorFromRGB import com.unciv.ui.objectdescriptions.BaseUnitDescriptions +import com.unciv.ui.objectdescriptions.BuildingDescriptions import com.unciv.ui.objectdescriptions.uniquesToCivilopediaTextLines import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen.Companion.showReligionInCivilopedia import com.unciv.ui.screens.civilopediascreen.FormattedLine @@ -192,22 +193,8 @@ class Nation : RulesetObject() { yield(FormattedLine("{${building.name}} -", link=building.makeLink())) if (building.replaces != null && ruleset.buildings.containsKey(building.replaces!!)) { val originalBuilding = ruleset.buildings[building.replaces!!]!! - yield(FormattedLine("Replaces [${originalBuilding.name}]", link=originalBuilding.makeLink(), indent=1)) - - for ((key, value) in building) - if (value != originalBuilding[key]) - yield(FormattedLine( key.name.tr() + " " +"[${value.toInt()}] vs [${originalBuilding[key].toInt()}]".tr(), indent=1)) - - for (unique in building.uniques.filter { it !in originalBuilding.uniques }) - yield(FormattedLine(unique, indent=1)) - if (building.maintenance != originalBuilding.maintenance) - yield(FormattedLine("{Maintenance} ".tr() + "[${building.maintenance}] vs [${originalBuilding.maintenance}]".tr(), indent=1)) - if (building.cost != originalBuilding.cost) - yield(FormattedLine("{Cost} ".tr() + "[${building.cost}] vs [${originalBuilding.cost}]".tr(), indent=1)) - if (building.cityStrength != originalBuilding.cityStrength) - yield(FormattedLine("{City strength} ".tr() + "[${building.cityStrength}] vs [${originalBuilding.cityStrength}]".tr(), indent=1)) - if (building.cityHealth != originalBuilding.cityHealth) - yield(FormattedLine("{City health} ".tr() + "[${building.cityHealth}] vs [${originalBuilding.cityHealth}]".tr(), indent=1)) + yield(FormattedLine("Replaces [${originalBuilding.name}]", link = originalBuilding.makeLink(), indent=1)) + yieldAll(BuildingDescriptions.getDifferences(ruleset, originalBuilding, building)) yield(FormattedLine()) } else if (building.replaces != null) { yield(FormattedLine("Replaces [${building.replaces}], which is not found in the ruleset!", indent=1)) diff --git a/core/src/com/unciv/ui/objectdescriptions/BaseUnitDescriptions.kt b/core/src/com/unciv/ui/objectdescriptions/BaseUnitDescriptions.kt index 9b8891716b..d043b00539 100644 --- a/core/src/com/unciv/ui/objectdescriptions/BaseUnitDescriptions.kt +++ b/core/src/com/unciv/ui/objectdescriptions/BaseUnitDescriptions.kt @@ -292,7 +292,8 @@ object BaseUnitDescriptions { val lostAbilityPredicate: (Unique)->Boolean = { it.text in betterUnit.uniques || it.isHiddenToUsers() } for (unique in originalUnit.uniqueObjects.filterNot(lostAbilityPredicate)) { - yield("Lost ability (vs [${originalUnit.name}]): [${unique.text}]" to null) + // Need double translation of the "ability" here - unique texts may contain nuts - pardon, square brackets + yield("Lost ability (vs [${originalUnit.name}]): [${unique.text.tr()}]" to null) } for (promotion in betterUnit.promotions.filter { it !in originalUnit.promotions }) { // Needs tr for **individual** translations (no bracket nesting), default separator would have extra blank diff --git a/core/src/com/unciv/ui/objectdescriptions/BuildingDescriptions.kt b/core/src/com/unciv/ui/objectdescriptions/BuildingDescriptions.kt new file mode 100644 index 0000000000..d22568d5b5 --- /dev/null +++ b/core/src/com/unciv/ui/objectdescriptions/BuildingDescriptions.kt @@ -0,0 +1,281 @@ +package com.unciv.ui.objectdescriptions + +import com.unciv.logic.city.City +import com.unciv.models.ruleset.Belief +import com.unciv.models.ruleset.Building +import com.unciv.models.ruleset.Ruleset +import com.unciv.models.ruleset.unique.StateForConditionals +import com.unciv.models.ruleset.unique.Unique +import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.models.stats.Stat +import com.unciv.models.translations.fillPlaceholders +import com.unciv.models.translations.tr +import com.unciv.ui.components.extensions.getConsumesAmountString +import com.unciv.ui.components.fonts.Fonts +import com.unciv.ui.screens.civilopediascreen.FormattedLine + +object BuildingDescriptions { + // Note: These are not extension functions for receiver Building because that would mean renaming getCivilopediaTextLines + // here, otherwise there is no override syntax for Building.getCivilopediaTextLines that can access the helper. + // To stay consistent, all take the Building as normal parameter instead. + + /** Used for AlertType.WonderBuilt, and as sub-text in Nation and Tech descriptions */ + fun getShortDescription(building: Building, multiline: Boolean = false): String = building.run { + val infoList = mutableListOf() + this.clone().toString().also { if (it.isNotEmpty()) infoList += it } + for ((key, value) in getStatPercentageBonuses(null)) + infoList += "+${value.toInt()}% ${key.name.tr()}" + + if (requiredNearbyImprovedResources != null) + infoList += "Requires worked [" + requiredNearbyImprovedResources!!.joinToString("/") { it.tr() } + "] near city" + if (uniques.isNotEmpty()) { + if (replacementTextForUniques.isNotEmpty()) infoList += replacementTextForUniques + else infoList += getUniquesStringsWithoutDisablers() + } + if (cityStrength != 0) infoList += "{City strength} +$cityStrength" + if (cityHealth != 0) infoList += "{City health} +$cityHealth" + val separator = if (multiline) "\n" else "; " + return infoList.joinToString(separator) { it.tr() } + } + + /** used in CityScreen (ConstructionInfoTable) */ + fun getDescription(building: Building, city: City, showAdditionalInfo: Boolean): String = building.run { + val stats = getStats(city) + val translatedLines = ArrayList() // Some translations require special handling + val isFree = city.civ.civConstructions.hasFreeBuilding(city, this) + if (uniqueTo != null) translatedLines += if (replaces == null) "Unique to [$uniqueTo]".tr() + else "Unique to [$uniqueTo], replaces [$replaces]".tr() + val missingUnique = getMatchingUniques(UniqueType.RequiresBuildingInAllCities).firstOrNull() + if (isWonder) translatedLines += "Wonder".tr() + if (isNationalWonder) translatedLines += "National Wonder".tr() + if (!isFree) { + for ((resourceName, amount) in getResourceRequirementsPerTurn(StateForConditionals(city.civ, city))) { + val available = city.getResourceAmount(resourceName) + val resource = city.getRuleset().tileResources[resourceName] ?: continue + val consumesString = resourceName.getConsumesAmountString(amount, resource.isStockpiled()) + + translatedLines += if (showAdditionalInfo) "$consumesString ({[$available] available})".tr() + else consumesString.tr() + } + } + + // Inefficient in theory. In practice, buildings seem to have only a small handful of uniques. + val missingCities = if (missingUnique != null) + // TODO: Unify with rejection reasons? + city.civ.cities.filterNot { + it.isPuppet + || it.cityConstructions.containsBuildingOrEquivalent(missingUnique.params[0]) + } + else listOf() + if (uniques.isNotEmpty()) { + if (replacementTextForUniques.isNotEmpty()) translatedLines += replacementTextForUniques.tr() + else translatedLines += getUniquesStringsWithoutDisablers( + filterUniques = if (missingCities.isEmpty()) null + else { unique -> !unique.isOfType(UniqueType.RequiresBuildingInAllCities) } + // Filter out the "Requires a [] in all cities" unique if any cities are still missing the required building, since in that case the list of cities will be appended at the end. + ).map { it.tr() } + } + if (!stats.isEmpty()) + translatedLines += stats.toString() + + for ((stat, value) in getStatPercentageBonuses(city)) + if (value != 0f) translatedLines += "+${value.toInt()}% {${stat.name}}".tr() + + for ((greatPersonName, value) in greatPersonPoints) + translatedLines += "+$value " + "[$greatPersonName] points".tr() + + for ((specialistName, amount) in newSpecialists()) + translatedLines += "+$amount " + "[$specialistName] slots".tr() + + if (requiredNearbyImprovedResources != null) + translatedLines += "Requires worked [${requiredNearbyImprovedResources!!.joinToString("/") { it.tr() }}] near city".tr() + + if (cityStrength != 0) translatedLines += "{City strength} +$cityStrength".tr() + if (cityHealth != 0) translatedLines += "{City health} +$cityHealth".tr() + if (maintenance != 0 && !isFree) translatedLines += "{Maintenance cost}: $maintenance {Gold}".tr() + if (showAdditionalInfo && missingCities.isNotEmpty()) { + // Could be red. But IMO that should be done by enabling GDX's ColorMarkupLanguage globally instead of adding a separate label. + translatedLines += "\n" + + "[${city.civ.getEquivalentBuilding(missingUnique!!.params[0])}] required:".tr() + + " " + missingCities.joinToString(", ") { it.name.tr(hideIcons = true) } + // Can't nest square bracket placeholders inside curlies, and don't see any way to define wildcard placeholders. So run translation explicitly on base text. + } + return translatedLines.joinToString("\n").trim() + } + + /** + * Lists differences: how a nation-unique Building compares to its replacement. + * + * Cost is **is** included. + * Result as indented, non-linking [FormattedLine]s + * + * @param originalBuilding The "standard" Building + * @param replacementBuilding The "uniqueTo" Building + */ + fun getDifferences( + ruleset: Ruleset, originalBuilding: Building, replacementBuilding: Building + ): Sequence = sequence { + for ((key, value) in replacementBuilding) + if (value != originalBuilding[key]) + yield(FormattedLine( key.name.tr() + " " +"[${value.toInt()}] vs [${originalBuilding[key].toInt()}]".tr(), indent=1)) + + if (replacementBuilding.maintenance != originalBuilding.maintenance) + yield(FormattedLine("{Maintenance} ".tr() + "[${replacementBuilding.maintenance}] vs [${originalBuilding.maintenance}]".tr(), indent=1)) + if (replacementBuilding.cost != originalBuilding.cost) + yield(FormattedLine("{Cost} ".tr() + "[${replacementBuilding.cost}] vs [${originalBuilding.cost}]".tr(), indent=1)) + if (replacementBuilding.cityStrength != originalBuilding.cityStrength) + yield(FormattedLine("{City strength} ".tr() + "[${replacementBuilding.cityStrength}] vs [${originalBuilding.cityStrength}]".tr(), indent=1)) + if (replacementBuilding.cityHealth != originalBuilding.cityHealth) + yield(FormattedLine("{City health} ".tr() + "[${replacementBuilding.cityHealth}] vs [${originalBuilding.cityHealth}]".tr(), indent=1)) + + if (replacementBuilding.replacementTextForUniques.isNotEmpty()) { + yield(FormattedLine(replacementBuilding.replacementTextForUniques, indent=1)) + } else { + val newAbilityPredicate: (Unique)->Boolean = { it.text in originalBuilding.uniques || it.isHiddenToUsers() } + for (unique in replacementBuilding.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 replacementBuilding.uniques || it.isHiddenToUsers() } + for (unique in originalBuilding.uniqueObjects.filterNot(lostAbilityPredicate)) { + // Need double translation of the "ability" here - unique texts may contain square brackets + yield(FormattedLine("Lost ability (vs [${originalBuilding.name}]): [${unique.text.tr()}]", indent=1)) + } + } + + fun getCivilopediaTextLines(building: Building, ruleset: Ruleset): List = building.run { + fun Float.formatSignedInt() = (if (this > 0f) "+" else "") + this.toInt().toString() + + val textList = ArrayList() + + if (isAnyWonder()) { + textList += FormattedLine( if (isWonder) "Wonder" else "National Wonder", color="#CA4", header=3 ) + } + + if (uniqueTo != null) { + textList += FormattedLine() + textList += FormattedLine("Unique to [$uniqueTo]", link="Nation/$uniqueTo") + if (replaces != null) { + val replacesBuilding = ruleset.buildings[replaces] + textList += FormattedLine("Replaces [$replaces]", link=replacesBuilding?.makeLink() ?: "", indent=1) + } + } + + if (cost > 0) { + val stats = mutableListOf("$cost${Fonts.production}") + if (canBePurchasedWithStat(null, Stat.Gold)) { + stats += "${getCivilopediaGoldCost()}${Fonts.gold}" + } + textList += FormattedLine(stats.joinToString("/", "{Cost}: ")) + } + + if (requiredTech != null) + textList += FormattedLine("Required tech: [$requiredTech]", + link="Technology/$requiredTech") + if (requiredBuilding != null) + textList += FormattedLine("Requires [$requiredBuilding] to be built in the city", + link="Building/$requiredBuilding") + + if (requiredResource != null) { + textList += FormattedLine() + val resource = ruleset.tileResources[requiredResource] + textList += FormattedLine( + requiredResource!!.getConsumesAmountString(1, resource!!.isStockpiled()), + link="Resources/$requiredResource", color="#F42" ) + } + + val stats = cloneStats() + val percentStats = getStatPercentageBonuses(null) + val specialists = newSpecialists() + if (uniques.isNotEmpty() || !stats.isEmpty() || !percentStats.isEmpty() || this.greatPersonPoints.isNotEmpty() || specialists.isNotEmpty()) + textList += FormattedLine() + + if (replacementTextForUniques.isNotEmpty()) { + textList += FormattedLine(replacementTextForUniques) + } else { + uniquesToCivilopediaTextLines(textList, colorConsumesResources = true) + } + + if (!stats.isEmpty()) { + textList += FormattedLine(stats.toString()) + } + + if (!percentStats.isEmpty()) { + for ((key, value) in percentStats) { + if (value == 0f) continue + textList += FormattedLine(value.formatSignedInt() + "% {$key}") + } + } + + for ((greatPersonName, value) in greatPersonPoints) { + textList += FormattedLine( + "+$value " + "[$greatPersonName] points".tr(), + link = "Unit/$greatPersonName" + ) + } + + if (specialists.isNotEmpty()) { + for ((specialistName, amount) in specialists) + textList += FormattedLine("+$amount " + "[$specialistName] slots".tr()) + } + + if (requiredNearbyImprovedResources != null) { + textList += FormattedLine() + textList += FormattedLine("Requires at least one of the following resources worked near the city:") + requiredNearbyImprovedResources!!.forEach { + textList += FormattedLine(it, indent = 1, link = "Resource/$it") + } + } + + if (cityStrength != 0 || cityHealth != 0 || maintenance != 0) textList += FormattedLine() + if (cityStrength != 0) textList += FormattedLine("{City strength} +$cityStrength") + if (cityHealth != 0) textList += FormattedLine("{City health} +$cityHealth") + if (maintenance != 0) textList += FormattedLine("{Maintenance cost}: $maintenance {Gold}") + + val seeAlso = ArrayList() + for (seeAlsoBuilding in ruleset.buildings.values) { + if (seeAlsoBuilding.replaces == name + || seeAlsoBuilding.uniqueObjects.any { unique -> unique.params.any { it == name } }) + seeAlso += FormattedLine(seeAlsoBuilding.name, link = seeAlsoBuilding.makeLink(), indent=1) + } + seeAlso += Belief.getCivilopediaTextMatching(name, ruleset, false) + if (seeAlso.isNotEmpty()) { + textList += FormattedLine() + textList += FormattedLine("{See also}:") + textList += seeAlso + } + + return textList + } + + /** + * @param filterUniques If provided, include only uniques for which this function returns true. + */ + private fun Building.getUniquesStrings(filterUniques: ((Unique) -> Boolean)? = null) = sequence { + val tileBonusHashmap = HashMap>() + for (unique in uniqueObjects) if (filterUniques == null || filterUniques(unique)) when { + unique.isOfType(UniqueType.StatsFromTiles) && unique.params[2] == "in this city" -> { + val stats = unique.params[0] + if (!tileBonusHashmap.containsKey(stats)) tileBonusHashmap[stats] = ArrayList() + tileBonusHashmap[stats]!!.add(unique.params[1]) + } + else -> yield(unique.text) + } + for ((key, value) in tileBonusHashmap) + yield( "[stats] from [tileFilter] tiles in this city" + .fillPlaceholders( key, + // A single tileFilter will be properly translated later due to being within [] + // advantage to not translate prematurely: FormatLine.formatUnique will recognize it + if (value.size == 1) value[0] else value.joinToString { it.tr() } + )) + } + + /** + * @param filterUniques If provided, include only uniques for which this function returns true. + */ + private fun Building.getUniquesStringsWithoutDisablers(filterUniques: ((Unique) -> Boolean)? = null) = getUniquesStrings { + !it.isHiddenToUsers() + && (filterUniques?.invoke(it) ?: true) + } + +}