From de12f671a54e5ce74022fb9a9144d211bdd14efb Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Sat, 11 Mar 2023 19:04:12 +0100 Subject: [PATCH] Improve completeness and consistency of Technology descriptions (#8860) --- .../unciv/models/ruleset/tech/Technology.kt | 266 +------------ .../com/unciv/models/ruleset/unit/BaseUnit.kt | 1 + .../com/unciv/models/ruleset/unit/UnitType.kt | 2 +- .../BaseUnitDescriptions.kt | 5 +- .../TechnologyDescriptions.kt | 370 ++++++++++++++++++ .../ui/screens/pickerscreens/TechButton.kt | 114 ++---- .../screens/pickerscreens/TechPickerScreen.kt | 3 +- .../ui/screens/worldscreen/AlertPopup.kt | 2 +- 8 files changed, 414 insertions(+), 349 deletions(-) rename core/src/com/unciv/{models/ruleset/unit => ui/objectdescriptions}/BaseUnitDescriptions.kt (98%) create mode 100644 core/src/com/unciv/ui/objectdescriptions/TechnologyDescriptions.kt diff --git a/core/src/com/unciv/models/ruleset/tech/Technology.kt b/core/src/com/unciv/models/ruleset/tech/Technology.kt index 0c29f53bcb..a3f219450d 100644 --- a/core/src/com/unciv/models/ruleset/tech/Technology.kt +++ b/core/src/com/unciv/models/ruleset/tech/Technology.kt @@ -1,18 +1,11 @@ package com.unciv.models.ruleset.tech -import com.unciv.GUI import com.unciv.logic.civilization.Civilization -import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetObject -import com.unciv.models.ruleset.RulesetStatsObject -import com.unciv.models.ruleset.unique.UniqueFlag import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.models.ruleset.unique.UniqueType -import com.unciv.models.ruleset.unit.BaseUnit -import com.unciv.models.translations.tr -import com.unciv.ui.components.Fonts -import com.unciv.ui.screens.civilopediascreen.FormattedLine +import com.unciv.ui.objectdescriptions.TechnologyDescriptions class Technology: RulesetObject() { @@ -29,261 +22,14 @@ class Technology: RulesetObject() { fun isContinuallyResearchable() = hasUnique(UniqueType.ResearchableMultipleTimes) - /** - * Textual description used in TechPickerScreen and AlertPopup(AlertType.TechResearched) - */ - fun getDescription(ruleset: Ruleset): String { - val lineList = ArrayList() // more readable than StringBuilder, with same performance for our use-case - for (unique in uniques) lineList += unique.tr() - - for (improvement in ruleset.tileImprovements.values) { - for (unique in improvement.getMatchingUniques(UniqueType.Stats)) { - val requiredTech = unique.conditionals.firstOrNull { it.isOfType(UniqueType.ConditionalTech) }?.params?.get(0) - if (requiredTech != name) continue - lineList += "[${unique.params[0]}] from every [${improvement.name}]" - } - - for (unique in improvement.getMatchingUniques(UniqueType.ImprovementStatsOnTile)) { - val requiredTech = unique.conditionals.firstOrNull { it.isOfType(UniqueType.ConditionalTech) }?.params?.get(0) - if (requiredTech != name) continue - lineList += "[${unique.params[0]}] from every [${improvement.name}] on [${unique.params[1]}] tiles" - } - } - - val viewingCiv = GUI.getViewingPlayer() - val enabledUnits = getEnabledUnits(ruleset, viewingCiv) - if (enabledUnits.any()) { - lineList += "{Units enabled}: " - for (unit in enabledUnits) - lineList += " • " + unit.name.tr() + " (" + unit.getShortDescription() + ")" - } - - val enabledBuildings = getEnabledBuildings(ruleset, viewingCiv) - - val regularBuildings = enabledBuildings.filter { !it.isAnyWonder() } - if (regularBuildings.any()) { - lineList += "{Buildings enabled}: " - for (building in regularBuildings) - lineList += " • " + building.name.tr() + " (" + building.getShortDescription() + ")" - } - - val wonders = enabledBuildings.filter { it.isAnyWonder() } - if (wonders.any()) { - lineList += "{Wonders enabled}: " - for (wonder in wonders) - lineList += " • " + wonder.name.tr() + " (" + wonder.getShortDescription() + ")" - } - - for (obj in getObsoletedObjects(ruleset, viewingCiv)) - lineList += "[${obj.name}] obsoleted" - - for (resource in ruleset.tileResources.values.asSequence().filter { it.revealedBy == name } - .map { it.name }) - lineList += "Reveals [$resource] on the map" - - val tileImprovements = ruleset.tileImprovements.values.filter { it.techRequired == name } - if (tileImprovements.isNotEmpty()) - lineList += "{Tile improvements enabled}: " + tileImprovements.joinToString { it.name.tr() } - - return lineList.joinToString("\n") { it.tr() } - } - - /** - * Returns a Sequence of [Building]s enabled by this Technology, filtered for [civInfo]'s uniques, - * nuclear weapons and religion settings, and without those expressly hidden from Civilopedia. - */ - // Used for Civilopedia, Alert and Picker, so if any of these decide to ignore the "Will not be displayed in Civilopedia" unique this needs refactoring - fun getEnabledBuildings(ruleset: Ruleset, civInfo: Civilization?) = getFilteredBuildings(ruleset, civInfo) - { it.requiredTech == name } - - /** - * Returns a Sequence of [Building]s obsoleted by this Technology, filtered for [civInfo]'s uniques, - * nuclear weapons and religion settings, and without those expressly hidden from Civilopedia. - */ - // Used for Civilopedia, Alert and Picker, so if any of these decide to ignore the "Will not be displayed in Civilopedia" unique this needs refactoring - - fun getObsoletedObjects(ruleset: Ruleset, civInfo: Civilization?): Sequence = - (getFilteredBuildings(ruleset, civInfo){true} - + ruleset.tileResources.values.asSequence() - + ruleset.tileImprovements.values.filter { - it.uniqueTo == null || it.uniqueTo == civInfo?.civName - }).filter { obj: RulesetStatsObject -> - obj.getMatchingUniques(UniqueType.ObsoleteWith).any { it.params[0] == name } - } - - // Helper: common filtering for both getEnabledBuildings and getObsoletedBuildings, difference via predicate parameter - private fun getFilteredBuildings( - ruleset: Ruleset, - civInfo: Civilization?, - predicate: (Building)->Boolean - ): Sequence { - val (nuclearWeaponsEnabled, religionEnabled) = getNukeAndReligionSwitches(civInfo) - return ruleset.buildings.values.asSequence() - .filter { - predicate(it) // expected to be the most selective, thus tested first - && (it.uniqueTo == civInfo?.civName || it.uniqueTo == null && civInfo?.getEquivalentBuilding(it) == it) - && (nuclearWeaponsEnabled || !it.hasUnique(UniqueType.EnablesNuclearWeapons)) - && (religionEnabled || !it.hasUnique(UniqueType.HiddenWithoutReligion)) - && !it.hasUnique(UniqueType.HiddenFromCivilopedia) - } - } - - private fun getNukeAndReligionSwitches(civInfo: Civilization?): Pair { - if (civInfo == null) return true to true - return civInfo.gameInfo.run { gameParameters.nuclearWeaponsEnabled to isReligionEnabled() } - } - - /** - * Returns a Sequence of [BaseUnit]s enabled by this Technology, filtered for [civInfo]'s uniques, - * nuclear weapons and religion settings, and without those expressly hidden from Civilopedia. - */ - // Used for Civilopedia, Alert and Picker, so if any of these decide to ignore the "Will not be displayed in Civilopedia"/HiddenFromCivilopedia unique this needs refactoring - fun getEnabledUnits(ruleset: Ruleset, civInfo: Civilization?): Sequence { - val (nuclearWeaponsEnabled, religionEnabled) = getNukeAndReligionSwitches(civInfo) - return ruleset.units.values.asSequence() - .filter { - it.requiredTech == name - && (it.uniqueTo == civInfo?.civName || it.uniqueTo == null && civInfo?.getEquivalentUnit(it) == it) - && (nuclearWeaponsEnabled || !it.isNuclearWeapon()) - && (religionEnabled || !it.hasUnique(UniqueType.HiddenWithoutReligion)) - && !it.hasUnique(UniqueType.HiddenFromCivilopedia) - } - } - - /** Get improvements related to this tech by a unique */ - private fun getSeeAlsoObjects(ruleset: Ruleset) = - // This is a band-aid to clarify the relation Offshore platform - Refrigeration. A generic variant - // covering all mentions in uniques in all ruleset objects would be possible here but - overkill for now. - ruleset.tileImprovements.values - .asSequence() - .filter { improvement -> - improvement.uniqueObjects.any { - unique -> unique.conditionals.any { - (it.isOfType(UniqueType.ConditionalTech) || it.isOfType(UniqueType.ConditionalNoTech)) - && it.params[0] == name - } - } - } - + /** Get Civilization-specific description for TechPicker or AlertType.TechResearched */ + fun getDescription(viewingCiv: Civilization) = + TechnologyDescriptions.getDescription(this, viewingCiv) override fun makeLink() = "Technology/$name" - override fun getCivilopediaTextLines(ruleset: Ruleset): List { - val lineList = ArrayList() - - val eraColor = ruleset.eras[era()]?.getHexColor() ?: "" - lineList += FormattedLine(era(), header = 3, color = eraColor) - lineList += FormattedLine() - lineList += FormattedLine("{Cost}: $cost${Fonts.science}") - - if (prerequisites.isNotEmpty()) { - lineList += FormattedLine() - if (prerequisites.size == 1) - prerequisites.first().let { lineList += FormattedLine("Required tech: [$it]", link = "Technology/$it") } - else { - lineList += FormattedLine("Requires all of the following:") - prerequisites.forEach { - lineList += FormattedLine(it, link = "Technology/$it") - } - } - } - - val leadsTo = ruleset.technologies.values.filter { name in it.prerequisites } - if (leadsTo.isNotEmpty()) { - lineList += FormattedLine() - if (leadsTo.size == 1) - leadsTo.first().let { lineList += FormattedLine("Leads to [${it.name}]", link = it.makeLink()) } - else { - lineList += FormattedLine("Leads to:") - leadsTo.forEach { - lineList += FormattedLine(it.name, link = it.makeLink()) - } - } - } - - if (uniques.isNotEmpty()) { - lineList += FormattedLine() - uniqueObjects.forEach { - if (!it.hasFlag(UniqueFlag.HiddenToUsers)) - lineList += FormattedLine(it) - } - } - - for (improvement in ruleset.tileImprovements.values) - for (unique in improvement.uniqueObjects) { - if (unique.isOfType(UniqueType.Stats)) { - val requiredTech = unique.conditionals.firstOrNull { it.isOfType(UniqueType.ConditionalTech) }?.params?.get(0) - if (requiredTech != name) continue - lineList += FormattedLine("[${unique.params[0]}] from every [${improvement.name}]", - link = improvement.makeLink()) - } else if (unique.placeholderText == "[] on [] tiles") { - val requiredTech = unique.conditionals.firstOrNull { it.isOfType(UniqueType.ConditionalTech) }?.params?.get(0) - if (requiredTech != name) continue - lineList += FormattedLine("[${unique.params[0]}] from every [${improvement.name}] on [${unique.params[1]}] tiles", - link = improvement.makeLink()) - } - } - - val viewingCiv = if (GUI.isWorldLoaded()) GUI.getViewingPlayer() else null - val enabledUnits = getEnabledUnits(ruleset, viewingCiv) - if (enabledUnits.any()) { - lineList += FormattedLine() - lineList += FormattedLine("{Units enabled}:") - for (unit in enabledUnits) - lineList += FormattedLine(unit.name.tr() + " (" + unit.getShortDescription() + ")", link = unit.makeLink()) - } - - val enabledBuildings = getEnabledBuildings(ruleset, viewingCiv) - .partition { it.isAnyWonder() } - if (enabledBuildings.first.isNotEmpty()) { - lineList += FormattedLine() - lineList += FormattedLine("{Wonders enabled}:") - for (wonder in enabledBuildings.first) - lineList += FormattedLine(wonder.name.tr() + " (" + wonder.getShortDescription() + ")", link = wonder.makeLink()) - } - if (enabledBuildings.second.isNotEmpty()) { - lineList += FormattedLine() - lineList += FormattedLine("{Buildings enabled}:") - for (building in enabledBuildings.second) - lineList += FormattedLine(building.name.tr() + " (" + building.getShortDescription() + ")", link = building.makeLink()) - } - - val obsoletedObjects = getObsoletedObjects(ruleset, viewingCiv) - if (obsoletedObjects.any()) { - lineList += FormattedLine() - obsoletedObjects.forEach { - lineList += FormattedLine("[${it.name}] obsoleted", link = it.makeLink()) - } - } - - val revealedResources = ruleset.tileResources.values.asSequence().filter { it.revealedBy == name } - if (revealedResources.any()) { - lineList += FormattedLine() - revealedResources.forEach { - lineList += FormattedLine("Reveals [${it.name}] on the map", link = it.makeLink()) - } - } - - val tileImprovements = ruleset.tileImprovements.values.asSequence().filter { it.techRequired == name } - if (tileImprovements.any()) { - lineList += FormattedLine() - lineList += FormattedLine("{Tile improvements enabled}:") - tileImprovements.forEach { - lineList += FormattedLine(it.name, link = it.makeLink()) - } - } - - val seeAlsoObjects = getSeeAlsoObjects(ruleset) - if (seeAlsoObjects.any()) { - lineList += FormattedLine() - lineList += FormattedLine("{See also}:") - seeAlsoObjects.forEach { - lineList += FormattedLine(it.name, link = it.makeLink()) - } - } - - return lineList - } + override fun getCivilopediaTextLines(ruleset: Ruleset) = + TechnologyDescriptions.getCivilopediaTextLines(this, ruleset) fun matchesFilter(filter: String): Boolean { return when (filter) { diff --git a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt index 65e64f8889..76723f0e6f 100644 --- a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt +++ b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt @@ -16,6 +16,7 @@ import com.unciv.ui.components.extensions.filterAndLogic import com.unciv.ui.components.extensions.getNeedMoreAmountString import com.unciv.ui.components.extensions.toPercent import com.unciv.ui.screens.civilopediascreen.FormattedLine +import com.unciv.ui.objectdescriptions.BaseUnitDescriptions import kotlin.math.pow // This is BaseUnit because Unit is already a base Kotlin class and to avoid mixing the two up diff --git a/core/src/com/unciv/models/ruleset/unit/UnitType.kt b/core/src/com/unciv/models/ruleset/unit/UnitType.kt index 7bcae749b0..df4b1ff7c9 100644 --- a/core/src/com/unciv/models/ruleset/unit/UnitType.kt +++ b/core/src/com/unciv/models/ruleset/unit/UnitType.kt @@ -3,7 +3,7 @@ package com.unciv.models.ruleset.unit import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetObject import com.unciv.models.ruleset.unique.UniqueTarget -import com.unciv.models.ruleset.unit.BaseUnitDescriptions.getUnitTypeCivilopediaTextLines +import com.unciv.ui.objectdescriptions.BaseUnitDescriptions.getUnitTypeCivilopediaTextLines enum class UnitLayer { // The layer in which the unit moves diff --git a/core/src/com/unciv/models/ruleset/unit/BaseUnitDescriptions.kt b/core/src/com/unciv/ui/objectdescriptions/BaseUnitDescriptions.kt similarity index 98% rename from core/src/com/unciv/models/ruleset/unit/BaseUnitDescriptions.kt rename to core/src/com/unciv/ui/objectdescriptions/BaseUnitDescriptions.kt index 4b171a70ec..33572e7c28 100644 --- a/core/src/com/unciv/models/ruleset/unit/BaseUnitDescriptions.kt +++ b/core/src/com/unciv/ui/objectdescriptions/BaseUnitDescriptions.kt @@ -1,9 +1,12 @@ -package com.unciv.models.ruleset.unit +package com.unciv.ui.objectdescriptions import com.unciv.logic.city.City import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.unique.UniqueFlag import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.models.ruleset.unit.BaseUnit +import com.unciv.models.ruleset.unit.UnitMovementType +import com.unciv.models.ruleset.unit.UnitType import com.unciv.models.stats.Stat import com.unciv.models.translations.tr import com.unciv.ui.screens.civilopediascreen.FormattedLine diff --git a/core/src/com/unciv/ui/objectdescriptions/TechnologyDescriptions.kt b/core/src/com/unciv/ui/objectdescriptions/TechnologyDescriptions.kt new file mode 100644 index 0000000000..f99e8b78ce --- /dev/null +++ b/core/src/com/unciv/ui/objectdescriptions/TechnologyDescriptions.kt @@ -0,0 +1,370 @@ +package com.unciv.ui.objectdescriptions + +import com.unciv.logic.civilization.Civilization +import com.unciv.models.ruleset.Building +import com.unciv.models.ruleset.Ruleset +import com.unciv.models.ruleset.RulesetStatsObject +import com.unciv.models.ruleset.tech.Technology +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.UniqueFlag +import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.models.ruleset.unit.BaseUnit +import com.unciv.models.translations.tr +import com.unciv.ui.components.Fonts +import com.unciv.ui.components.extensions.center +import com.unciv.ui.images.ImageGetter +import com.unciv.ui.screens.civilopediascreen.FormattedLine +import com.unciv.ui.screens.civilopediascreen.ICivilopediaText +import com.unciv.ui.screens.pickerscreens.TechButton + + +object TechnologyDescriptions { + //region Methods called from Technology + /** + * Textual description used in TechPickerScreen and AlertPopup(AlertType.TechResearched) - + * Civilization always known and description tailored to + */ + fun getDescription(technology: Technology, viewingCiv: Civilization): String = technology.run { + val ruleset = viewingCiv.gameInfo.ruleset + val lineList = ArrayList() // more readable than StringBuilder, with same performance for our use-case + for (unique in uniques) lineList += unique + + lineList.addAll( + getAffectedImprovements(name, ruleset) + .filter { it.improvement.uniqueTo == null || it.improvement.uniqueTo == viewingCiv.civName } + .map { it.getText() } + ) + + val enabledUnits = getEnabledUnits(name, ruleset, viewingCiv) + if (enabledUnits.any()) { + lineList += "{Units enabled}: " + for (unit in enabledUnits) + lineList += " • " + unit.name.tr() + " (" + unit.getShortDescription() + ")" + } + + val (wonders, regularBuildings) = getEnabledBuildings(name, ruleset, viewingCiv) + .partition { it.isAnyWonder() } + + if (regularBuildings.isNotEmpty()) { + lineList += "{Buildings enabled}: " + for (building in regularBuildings) + lineList += " • " + building.name.tr() + " (" + building.getShortDescription() + ")" + } + + if (wonders.isNotEmpty()) { + lineList += "{Wonders enabled}: " + for (wonder in wonders) + lineList += " • " + wonder.name.tr() + " (" + wonder.getShortDescription() + ")" + } + + for (obj in getObsoletedObjects(name, ruleset, viewingCiv)) + lineList += "[${obj.name}] obsoleted" + + val resourcesRevealed = ruleset.tileResources.values.asSequence() + .filter { it.revealedBy == name } + .map { it.name } + for (resource in resourcesRevealed) + lineList += "Reveals [$resource] on the map" + + val tileImprovements = ruleset.tileImprovements.values.asSequence() + .filter { it.techRequired == name } + .filter { it.uniqueTo == null || it.uniqueTo == viewingCiv.civName } + .toList() + if (tileImprovements.isNotEmpty()) + lineList += "{Tile improvements enabled}: " + tileImprovements.joinToString { it.name.tr() } + + return lineList.joinToString("\n") { it.tr() } + } + + /** + * Gets icons to display on a [TechButton] - all should be also described in [getDescription] + */ + fun getTechEnabledIcons(tech: Technology, viewingCiv: Civilization, techIconSize: Float) = sequence { + val ruleset = viewingCiv.gameInfo.ruleset + val techName = tech.name + val civName = viewingCiv.civName + + for (unit in getEnabledUnits(techName, ruleset, viewingCiv)) { + yield(ImageGetter.getConstructionPortrait(unit.name, techIconSize)) + } + + for (building in getEnabledBuildings(techName, ruleset, viewingCiv)) { + yield(ImageGetter.getConstructionPortrait(building.name, techIconSize)) + } + + yieldAll( + getObsoletedObjects(techName, ruleset, viewingCiv) + .mapNotNull { it.getObsoletedIcon(techIconSize) } + ) + + for (resource in ruleset.tileResources.values.filter { it.revealedBy == techName }) { + yield(ImageGetter.getResourcePortrait(resource.name, techIconSize)) + } + + for (improvement in ruleset.tileImprovements.values.asSequence() + .filter { it.techRequired == techName } + .filter { it.uniqueTo == null || it.uniqueTo == civName } + ) { + yield(ImageGetter.getImprovementPortrait(improvement.name, techIconSize)) + } + + for (improvement in ruleset.tileImprovements.values.asSequence() + .filter { it.uniqueObjects.any { u -> u.allParams.contains(techName) } } + .filter { it.uniqueTo == null || it.uniqueTo == civName } + ) { + yield(ImageGetter.getUniquePortrait(improvement.name, techIconSize)) + } + + for (unique in tech.uniqueObjects) { + yield( + when { + unique.isOfType(UniqueType.EnablesCivWideStatProduction) -> + ImageGetter.getConstructionPortrait(unique.params[0], techIconSize) + else -> + ImageGetter.getUniquePortrait(unique.text, techIconSize) + } + ) + } + } + + /** + * Implementation of [ICivilopediaText.getCivilopediaTextLines] + */ + fun getCivilopediaTextLines(technology: Technology, ruleset: Ruleset): List = technology.run { + val lineList = ArrayList() + + val eraColor = ruleset.eras[era()]?.getHexColor() ?: "" + lineList += FormattedLine(era(), header = 3, color = eraColor) + lineList += FormattedLine() + lineList += FormattedLine("{Cost}: $cost${Fonts.science}") + + if (prerequisites.isNotEmpty()) { + lineList += FormattedLine() + if (prerequisites.size == 1) + prerequisites.first().let { lineList += FormattedLine("Required tech: [$it]", link = "Technology/$it") } + else { + lineList += FormattedLine("Requires all of the following:") + prerequisites.forEach { + lineList += FormattedLine(it, link = "Technology/$it") + } + } + } + + val leadsTo = ruleset.technologies.values.filter { name in it.prerequisites } + if (leadsTo.isNotEmpty()) { + lineList += FormattedLine() + if (leadsTo.size == 1) + leadsTo.first().let { lineList += FormattedLine("Leads to [${it.name}]", link = it.makeLink()) } + else { + lineList += FormattedLine("Leads to:") + leadsTo.forEach { + lineList += FormattedLine(it.name, link = it.makeLink()) + } + } + } + + if (uniques.isNotEmpty()) { + lineList += FormattedLine() + uniqueObjects.forEach { + if (!it.hasFlag(UniqueFlag.HiddenToUsers)) + lineList += FormattedLine(it) + } + } + + val affectedImprovements = getAffectedImprovements(name, ruleset) + if (affectedImprovements.any()) { + lineList += FormattedLine() + for (entry in affectedImprovements) { + lineList += FormattedLine(entry.getText(), link = entry.improvement.makeLink()) + } + } + + val enabledUnits = getEnabledUnits(name, ruleset, null) + if (enabledUnits.any()) { + lineList += FormattedLine() + lineList += FormattedLine("{Units enabled}:") + for (unit in enabledUnits) + lineList += FormattedLine(unit.name.tr() + " (" + unit.getShortDescription() + ")", link = unit.makeLink()) + } + + val (wonders, regularBuildings) = getEnabledBuildings(name, ruleset, null) + .partition { it.isAnyWonder() } + + if (wonders.isNotEmpty()) { + lineList += FormattedLine() + lineList += FormattedLine("{Wonders enabled}:") + for (wonder in wonders) + lineList += FormattedLine(wonder.name.tr() + " (" + wonder.getShortDescription() + ")", link = wonder.makeLink()) + } + + if (regularBuildings.isNotEmpty()) { + lineList += FormattedLine() + lineList += FormattedLine("{Buildings enabled}:") + for (building in regularBuildings) + lineList += FormattedLine(building.name.tr() + " (" + building.getShortDescription() + ")", link = building.makeLink()) + } + + val obsoletedObjects = getObsoletedObjects(name, ruleset, null).toList() + if (obsoletedObjects.isNotEmpty()) { + lineList += FormattedLine() + obsoletedObjects.forEach { + lineList += FormattedLine("[${it.name}] obsoleted", link = it.makeLink()) + } + } + + val revealedResources = ruleset.tileResources.values.asSequence().filter { it.revealedBy == name } + if (revealedResources.any()) { + lineList += FormattedLine() + revealedResources.forEach { + lineList += FormattedLine("Reveals [${it.name}] on the map", link = it.makeLink()) + } + } + + val tileImprovements = ruleset.tileImprovements.values.asSequence().filter { it.techRequired == name } + if (tileImprovements.any()) { + lineList += FormattedLine() + lineList += FormattedLine("{Tile improvements enabled}:") + tileImprovements.forEach { + lineList += FormattedLine(it.name, link = it.makeLink()) + } + } + + val seeAlsoObjects = getSeeAlsoObjects(ruleset) + if (seeAlsoObjects.isNotEmpty()) { + lineList += FormattedLine() + lineList += FormattedLine("{See also}:") + seeAlsoObjects.forEach { + lineList += FormattedLine(it.name, link = it.makeLink()) + } + } + + return lineList + } + + //endregion + //region Helpers + + /** + * Returns a Sequence of [Building]s enabled by this Technology, filtered for [civInfo]'s uniques, + * nuclear weapons and religion settings, and without those expressly hidden from Civilopedia. + */ + // Used for Civilopedia, Alert and Picker, so if any of these decide to ignore the "Will not be displayed in Civilopedia" unique this needs refactoring + private fun getEnabledBuildings(techName: String, ruleset: Ruleset, civInfo: Civilization?) = + getFilteredBuildings(ruleset, civInfo) { it.requiredTech == techName } + + /** + * Returns a Sequence of [RulesetStatsObject]s obsoleted by this Technology, filtered for [civInfo]'s uniques, + * nuclear weapons and religion settings, and without those expressly hidden from Civilopedia. + */ + // Used for Civilopedia, Alert and Picker, so if any of these decide to ignore the "Will not be displayed in Civilopedia" unique this needs refactoring + private fun getObsoletedObjects(techName: String, ruleset: Ruleset, civInfo: Civilization?): Sequence = + ( + getFilteredBuildings(ruleset, civInfo) { true } + + ruleset.tileResources.values.asSequence() + + ruleset.tileImprovements.values.filter { + it.uniqueTo == null || it.uniqueTo == civInfo?.civName + } + ).filter { obj: RulesetStatsObject -> + obj.getMatchingUniques(UniqueType.ObsoleteWith).any { it.params[0] == techName } + } + + /** Readability - for the 'obsoleted' in [getTechEnabledIcons] */ + private fun RulesetStatsObject.getObsoletedIcon(techIconSize: Float) = + when (this) { + is Building -> ImageGetter.getConstructionPortrait(name, techIconSize) + is TileResource -> ImageGetter.getResourcePortrait(name, techIconSize) + is TileImprovement -> ImageGetter.getImprovementPortrait(name, techIconSize) + else -> null + }?.also { + val closeImage = ImageGetter.getRedCross(techIconSize / 2, 1f) + closeImage.center(it) + it.addActor(closeImage) + } + + /** Common filtering for both [getEnabledBuildings] and [getObsoletedObjects], difference via predicate parameter */ + private fun getFilteredBuildings( + ruleset: Ruleset, + civInfo: Civilization?, + predicate: (Building) -> Boolean + ): Sequence { + val (nuclearWeaponsEnabled, religionEnabled) = getNukeAndReligionSwitches(civInfo) + return ruleset.buildings.values.asSequence() + .filter { + predicate(it) // expected to be the most selective, thus tested first + && (it.uniqueTo == civInfo?.civName || it.uniqueTo == null && civInfo?.getEquivalentBuilding(it) == it) + && (nuclearWeaponsEnabled || !it.hasUnique(UniqueType.EnablesNuclearWeapons)) + && (religionEnabled || !it.hasUnique(UniqueType.HiddenWithoutReligion)) + && !it.hasUnique(UniqueType.HiddenFromCivilopedia) + } + } + + private fun getNukeAndReligionSwitches(civInfo: Civilization?): Pair { + if (civInfo == null) return true to true + return civInfo.gameInfo.run { gameParameters.nuclearWeaponsEnabled to isReligionEnabled() } + } + + /** + * Returns a Sequence of [BaseUnit]s enabled by this Technology, filtered for [civInfo]'s uniques, + * nuclear weapons and religion settings, and without those expressly hidden from Civilopedia. + */ + // Used for Civilopedia, Alert and Picker, so if any of these decide to ignore the "Will not be displayed in Civilopedia"/HiddenFromCivilopedia unique this needs refactoring + private fun getEnabledUnits(techName: String, ruleset: Ruleset, civInfo: Civilization?): Sequence { + val (nuclearWeaponsEnabled, religionEnabled) = getNukeAndReligionSwitches(civInfo) + return ruleset.units.values.asSequence() + .filter { + it.requiredTech == techName + && (it.uniqueTo == civInfo?.civName || it.uniqueTo == null && civInfo?.getEquivalentUnit(it) == it) + && (nuclearWeaponsEnabled || !it.isNuclearWeapon()) + && (religionEnabled || !it.hasUnique(UniqueType.HiddenWithoutReligion)) + && !it.hasUnique(UniqueType.HiddenFromCivilopedia) + } + } + + /** Tests whether a Unique means bonus Stats enabled by [techName] */ + private fun Unique.isImprovementStatsEnabledByTech(techName: String) = + (isOfType(UniqueType.Stats) || isOfType(UniqueType.ImprovementStatsOnTile)) && + conditionals.any { + it.isOfType(UniqueType.ConditionalTech) && it.params[0] == techName + } + + /** Tests whether a Unique Conditional is enabling or disabling its parent by a tech */ + private fun Unique.isTechConditional() = + isOfType(UniqueType.ConditionalTech) || + isOfType(UniqueType.ConditionalNoTech) + + /** Tests whether a Unique is enabled or disabled by [techName] */ + private fun Unique.isRelatedToTech(techName: String) = + conditionals.any { + it.isTechConditional() && it.params[0] == techName + } + + /** Used by [getAffectedImprovements] only */ + private data class ImprovementAndUnique(val improvement: TileImprovement, val unique: Unique) { + fun getText() = "[${unique.params[0]}] from every [${improvement.name}]" + + (if (unique.isOfType(UniqueType.Stats)) "" else " on [${unique.params[1]}] tiles") + } + + /** Yields Improvements with bonus Stats enabled by [techName] including the Unique doing it */ + private fun getAffectedImprovements(techName: String, ruleset: Ruleset): Sequence = + ruleset.tileImprovements.values.asSequence() + .flatMap { improvement -> + improvement.uniqueObjects.asSequence() + .filter { it.isImprovementStatsEnabledByTech(techName) } + .map { ImprovementAndUnique(improvement, it) } + } + + /** Get other objects related to this tech by a unique, + * e.g. Petra/Archaeology or Work Boats/Astronomy + * but not including the Improvement Stats increases already listed */ + private fun Technology.getSeeAlsoObjects(ruleset: Ruleset) = + ruleset.allRulesetObjects() + .filter { iRulesetObject -> + iRulesetObject.uniqueObjects.any { + it.isRelatedToTech(name) && !it.isImprovementStatsEnabledByTech(name) + } + }.toList() + + //endregion +} diff --git a/core/src/com/unciv/ui/screens/pickerscreens/TechButton.kt b/core/src/com/unciv/ui/screens/pickerscreens/TechButton.kt index bc86e6a652..aea26fd950 100644 --- a/core/src/com/unciv/ui/screens/pickerscreens/TechButton.kt +++ b/core/src/com/unciv/ui/screens/pickerscreens/TechButton.kt @@ -1,16 +1,12 @@ package com.unciv.ui.screens.pickerscreens import com.badlogic.gdx.graphics.Color -import com.badlogic.gdx.scenes.scene2d.Group import com.badlogic.gdx.scenes.scene2d.Touchable import com.badlogic.gdx.scenes.scene2d.ui.Image import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.utils.Align import com.unciv.logic.civilization.managers.TechManager -import com.unciv.models.ruleset.Building -import com.unciv.models.ruleset.tile.TileImprovement -import com.unciv.models.ruleset.tile.TileResource -import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.ui.objectdescriptions.TechnologyDescriptions import com.unciv.ui.images.ImageGetter import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.components.extensions.addBorder @@ -21,29 +17,38 @@ import com.unciv.ui.components.extensions.darken import com.unciv.ui.components.extensions.setFontSize import com.unciv.ui.components.extensions.toLabel -class TechButton(techName:String, private val techManager: TechManager, isWorldScreen: Boolean = true) : Table( - BaseScreen.skin) { - val text = "".toLabel().apply { +class TechButton( + techName: String, + private val techManager: TechManager, + isWorldScreen: Boolean = true +) : Table(BaseScreen.skin) { + + internal val text = "".toLabel().apply { wrap = false setFontSize(14) setAlignment(Align.left) setEllipsis(true) } - val turns = "".toLabel().apply { + + internal val turns = "".toLabel().apply { setFontSize(14) setAlignment(Align.right) } - var bg = Image(BaseScreen.skinStrings.getUiBackground("TechPickerScreen/TechButton", BaseScreen.skinStrings.roundedEdgeRectangleMidShape)) + + private val backgroundImage: Image // Table.background is the border init { touchable = Touchable.enabled - background = BaseScreen.skinStrings.getUiBackground("TechPickerScreen/TechButton", BaseScreen.skinStrings.roundedEdgeRectangleMidShape, - tintColor = Color.WHITE.cpy().darken(0.3f)) - bg.toBack() - addActor(bg) + val path = "TechPickerScreen/TechButton" + val default = BaseScreen.skinStrings.roundedEdgeRectangleMidShape + backgroundImage = Image(BaseScreen.skinStrings.getUiBackground(path, default)) + background = BaseScreen.skinStrings.getUiBackground(path, default, Color.WHITE.darken(0.3f)) - pad(0f).padBottom(5f).padTop(5f).padLeft(5f).padRight(0f) + backgroundImage.toBack() + addActor(backgroundImage) + + pad(5f, 5f, 5f, 0f) add(ImageGetter.getTechIconPortrait(techName, 46f)) .padRight(5f).padLeft(2f).left() @@ -73,15 +78,15 @@ class TechButton(techName:String, private val techManager: TechManager, isWorldS add(rightSide).expandX().left() pack() - bg.setSize(width-3f, height-3f) - bg.align = Align.center - bg.center(this) + backgroundImage.setSize(width - 3f, height - 3f) + backgroundImage.align = Align.center + backgroundImage.center(this) pack() } fun setButtonColor(color: Color) { - bg.color = color + backgroundImage.color = color pack() } @@ -92,74 +97,15 @@ class TechButton(techName:String, private val techManager: TechManager, isWorldS BaseScreen.skinStrings.roundedEdgeRectangleSmallShape, tintColor = Color.BLACK.cpy().apply { a = 0.7f } ) - techEnabledIcons.pad(0f).padLeft(10f).padTop(2f).padBottom(2f) + techEnabledIcons.pad(2f, 10f, 2f, 0f) techEnabledIcons.defaults().padRight(5f) - val techIconSize = 30f - val civName = techManager.civInfo.civName - val ruleset = techManager.civInfo.gameInfo.ruleset + val civ = techManager.civInfo + val tech = civ.gameInfo.ruleset.technologies[techName]!! - val tech = ruleset.technologies[techName]!! - - val icons = ArrayList() - - for (unit in tech.getEnabledUnits(ruleset, techManager.civInfo)) { - icons.add(ImageGetter.getConstructionPortrait(unit.name, techIconSize)) - } - - for (building in tech.getEnabledBuildings(ruleset, techManager.civInfo)) { - icons.add(ImageGetter.getConstructionPortrait(building.name, techIconSize)) - } - - for (obj in tech.getObsoletedObjects(ruleset, techManager.civInfo)) { - val obsoletedIcon = when (obj) { - is Building -> ImageGetter.getConstructionPortrait(obj.name, techIconSize) - is TileResource -> ImageGetter.getResourcePortrait(obj.name, techIconSize) - is TileImprovement -> ImageGetter.getImprovementPortrait(obj.name, techIconSize) - else -> continue - }.also { - val closeImage = ImageGetter.getRedCross(techIconSize / 2, 1f) - closeImage.center(it) - it.addActor(closeImage) - } - icons.add(obsoletedIcon) - } - - for (resource in ruleset.tileResources.values.filter { it.revealedBy == techName }) { - icons.add(ImageGetter.getResourcePortrait(resource.name, techIconSize)) - } - - for (improvement in ruleset.tileImprovements.values.asSequence() - .filter { it.techRequired == techName } - .filter { it.uniqueTo == null || it.uniqueTo == civName } - ) { - icons.add(ImageGetter.getImprovementPortrait(improvement.name, techIconSize)) - } - - for (improvement in ruleset.tileImprovements.values.asSequence() - .filter { it.uniqueObjects.any { u -> u.allParams.contains(techName) } } - .filter { it.uniqueTo == null || it.uniqueTo == civName } - ) { - icons.add(ImageGetter.getUniquePortrait(improvement.name, techIconSize)) - } - - for (unique in tech.uniques) { - icons.add( - when (unique) { - UniqueType.EnablesCivWideStatProduction.text.replace("civWideStat", "Gold" ) - -> ImageGetter.getConstructionPortrait("Gold", techIconSize) - UniqueType.EnablesCivWideStatProduction.text.replace("civWideStat", "Science" ) - -> ImageGetter.getConstructionPortrait("Science", techIconSize) - else -> ImageGetter.getUniquePortrait(unique, techIconSize) - } - ) - } - - for (i in 0..4) { - val icon = icons.getOrNull(i) - if (icon != null) - techEnabledIcons.add(icon) - } + TechnologyDescriptions.getTechEnabledIcons(tech, civ, techIconSize = 30f) + .take(5) + .forEach { techEnabledIcons.add(it) } rightSide.add(techEnabledIcons) .colspan(2) diff --git a/core/src/com/unciv/ui/screens/pickerscreens/TechPickerScreen.kt b/core/src/com/unciv/ui/screens/pickerscreens/TechPickerScreen.kt index 8ed6ed740f..f0f608a3ab 100644 --- a/core/src/com/unciv/ui/screens/pickerscreens/TechPickerScreen.kt +++ b/core/src/com/unciv/ui/screens/pickerscreens/TechPickerScreen.kt @@ -9,7 +9,6 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.utils.Align import com.unciv.Constants import com.unciv.GUI -import com.unciv.UncivGame import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.managers.TechManager import com.unciv.models.UncivSound @@ -370,7 +369,7 @@ class TechPickerScreen( val previousSelectedTech = selectedTech selectedTech = tech - descriptionLabel.setText(tech?.getDescription(civInfo.gameInfo.ruleset)) + descriptionLabel.setText(tech?.getDescription(civInfo)) if (!switchFromWorldScreen) return diff --git a/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt b/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt index dda21d7045..1716549e19 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt @@ -229,7 +229,7 @@ class AlertPopup(val worldScreen: WorldScreen, val popupAlert: PopupAlert): Popu val centerTable = Table() centerTable.add(tech.quote.toLabel().apply { wrap = true }).width(worldScreen.stage.width / 3) centerTable.add(ImageGetter.getTechIconPortrait(tech.name, 100f)).pad(20f) - val descriptionScroll = ScrollPane(tech.getDescription(gameBasics).toLabel().apply { wrap = true }) + val descriptionScroll = ScrollPane(tech.getDescription(worldScreen.viewingCiv).toLabel().apply { wrap = true }) centerTable.add(descriptionScroll).width(worldScreen.stage.width / 3).maxHeight(worldScreen.stage.height / 2) add(centerTable).row() add(getCloseButton(Constants.close))