From 9683e27526af844b43c2bdb9ff4af0890809c830 Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Sun, 19 Jun 2022 02:29:07 +0200 Subject: [PATCH] Civilopedia tweaks (#7051) * Civilopedia Files Split * Central showReligionInCivilopedia function * ConstructImprovementConsumingUnit with Conditional for Prophet * Show Units capable of building an Improvement * "Needs removal of terrain features to be built" --- .../jsons/Civ V - Gods & Kings/Units.json | 12 +- .../assets/jsons/Civ V - Vanilla/Units.json | 12 +- .../jsons/translations/template.properties | 3 + core/src/com/unciv/models/ruleset/Belief.kt | 6 +- core/src/com/unciv/models/ruleset/Nation.kt | 4 +- .../models/ruleset/tile/TileImprovement.kt | 114 ++++++++--- .../com/unciv/models/ruleset/unique/Unique.kt | 3 + .../unciv/models/ruleset/unique/UniqueType.kt | 3 +- .../ui/civilopedia/CivilopediaCategories.kt | 1 + .../unciv/ui/civilopedia/CivilopediaScreen.kt | 16 +- .../{CivilopediaText.kt => FormattedLine.kt} | 177 +----------------- .../unciv/ui/civilopedia/ICivilopediaText.kt | 101 ++++++++++ .../unciv/ui/civilopedia/MarkupRenderer.kt | 65 +++++++ .../ui/civilopedia/SimpleCivilopediaText.kt | 13 ++ 14 files changed, 313 insertions(+), 217 deletions(-) rename core/src/com/unciv/ui/civilopedia/{CivilopediaText.kt => FormattedLine.kt} (66%) create mode 100644 core/src/com/unciv/ui/civilopedia/ICivilopediaText.kt create mode 100644 core/src/com/unciv/ui/civilopedia/MarkupRenderer.kt create mode 100644 core/src/com/unciv/ui/civilopedia/SimpleCivilopediaText.kt diff --git a/android/assets/jsons/Civ V - Gods & Kings/Units.json b/android/assets/jsons/Civ V - Gods & Kings/Units.json index 4f4cef175b..ebc20a8114 100644 --- a/android/assets/jsons/Civ V - Gods & Kings/Units.json +++ b/android/assets/jsons/Civ V - Gods & Kings/Units.json @@ -540,7 +540,7 @@ "uniques": ["[+50]% Strength "], "upgradesTo": "Lancer", "obsoleteTech": "Gunpowder", - "attackSound": "metalhit" + "attackSound": "metalhit" }, { "name": "Landsknecht", @@ -1623,10 +1623,10 @@ { "name": "Great Prophet", "unitType": "Civilian", - "uniques": ["Can construct [Holy site] if it hasn't used other actions yet", "Can [Spread Religion] [4] times", - "Removes other religions when spreading religion", "May found a religion", "May enhance a religion", - "May enter foreign tiles without open borders", "[-1] Sight", "Great Person - [Faith]", - "Unbuildable", "Religious Unit", "Hidden when religion is disabled", + "uniques": ["Can construct [Holy site] ", "Can [Spread Religion] [4] times", + "Removes other religions when spreading religion", "May found a religion", "May enhance a religion", + "May enter foreign tiles without open borders", "[-1] Sight", "Great Person - [Faith]", + "Unbuildable", "Religious Unit", "Hidden when religion is disabled", "Takes your religion over the one in their birth city"], "movement": 2, "religiousStrength": 1000 @@ -1654,7 +1654,7 @@ "name": "Missionary", "unitType": "Civilian", "uniques": ["Can [Spread Religion] [2] times", "May enter foreign tiles without open borders, but loses [250] religious strength each turn it ends there", - "Can be purchased with [Faith] [in all cities in which the majority religion is a major religion]", + "Can be purchased with [Faith] [in all cities in which the majority religion is a major religion]", "[-1] Sight", "Unbuildable", "Religious Unit", "Hidden when religion is disabled"], "movement": 4, "religiousStrength": 1000 diff --git a/android/assets/jsons/Civ V - Vanilla/Units.json b/android/assets/jsons/Civ V - Vanilla/Units.json index 75f218b9d6..93b9be1866 100644 --- a/android/assets/jsons/Civ V - Vanilla/Units.json +++ b/android/assets/jsons/Civ V - Vanilla/Units.json @@ -408,7 +408,7 @@ "uniques": ["[+50]% Strength "], "upgradesTo": "Lancer", "obsoleteTech": "Gunpowder", - "attackSound": "metalhit" + "attackSound": "metalhit" }, { "name": "Landsknecht", @@ -1299,10 +1299,10 @@ { "name": "Great Prophet", "unitType": "Civilian", - "uniques": ["Can construct [Holy site] if it hasn't used other actions yet", "Can [Spread Religion] [4] times", - "Removes other religions when spreading religion", "May found a religion", "May enhance a religion", - "May enter foreign tiles without open borders", "[-1] Sight", "Great Person - [Faith]", - "Unbuildable", "Religious Unit", "Hidden when religion is disabled", + "uniques": ["Can construct [Holy site] ", "Can [Spread Religion] [4] times", + "Removes other religions when spreading religion", "May found a religion", "May enhance a religion", + "May enter foreign tiles without open borders", "[-1] Sight", "Great Person - [Faith]", + "Unbuildable", "Religious Unit", "Hidden when religion is disabled", "Takes your religion over the one in their birth city"], "movement": 2, "religiousStrength": 1000 @@ -1330,7 +1330,7 @@ "name": "Missionary", "unitType": "Civilian", "uniques": ["Can [Spread Religion] [2] times", "May enter foreign tiles without open borders, but loses [250] religious strength each turn it ends there", - "Can be purchased with [Faith] [in all cities in which the majority religion is a major religion]", + "Can be purchased with [Faith] [in all cities in which the majority religion is a major religion]", "[-1] Sight", "Unbuildable", "Religious Unit", "Hidden when religion is disabled"], "movement": 4, "religiousStrength": 1000 diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 1ba999a7e5..0efc3050db 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -1267,6 +1267,7 @@ Units that consume this resource = Can be built on = or [terrainType] = Can be constructed by = +Can be created instantly by = Defence bonus = Movement cost = for = @@ -1348,6 +1349,8 @@ Peace deal duration: [amount] turns⏳ = Start year: [comment] = Pillaging this improvement yields [stats] = Pillaging this improvement yields approximately [stats] = +Needs removal of terrain features to be built = + # Policies diff --git a/core/src/com/unciv/models/ruleset/Belief.kt b/core/src/com/unciv/models/ruleset/Belief.kt index e4c31a0a86..a8220d09e7 100644 --- a/core/src/com/unciv/models/ruleset/Belief.kt +++ b/core/src/com/unciv/models/ruleset/Belief.kt @@ -5,6 +5,7 @@ import com.unciv.UncivGame import com.unciv.models.ruleset.unique.UniqueFlag import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.models.translations.tr +import com.unciv.ui.civilopedia.CivilopediaScreen.Companion.showReligionInCivilopedia import com.unciv.ui.civilopedia.FormattedLine import kotlin.collections.ArrayList @@ -46,10 +47,7 @@ class Belief() : RulesetObject() { companion object { // private but potentially reusable, therefore not folded into getCivilopediaTextMatching private fun getBeliefsMatching(name: String, ruleset: Ruleset): Sequence { - if (!UncivGame.isCurrentInitialized()) return sequenceOf() - val gameInfo = UncivGame.Current.gameInfo - if (gameInfo == null) return sequenceOf() - if (!gameInfo.isReligionEnabled()) return sequenceOf() + if (!showReligionInCivilopedia(ruleset)) return sequenceOf() return ruleset.beliefs.asSequence().map { it.value } .filter { belief -> belief.uniqueObjects.any { unique -> unique.params.any { it == name } } } diff --git a/core/src/com/unciv/models/ruleset/Nation.kt b/core/src/com/unciv/models/ruleset/Nation.kt index 90b241a689..a61ab2e520 100644 --- a/core/src/com/unciv/models/ruleset/Nation.kt +++ b/core/src/com/unciv/models/ruleset/Nation.kt @@ -10,6 +10,7 @@ import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.translations.squareBraceRegex import com.unciv.models.translations.tr +import com.unciv.ui.civilopedia.CivilopediaScreen.Companion.showReligionInCivilopedia import com.unciv.ui.civilopedia.FormattedLine import com.unciv.ui.utils.Fonts import com.unciv.ui.utils.extensions.colorFromRGB @@ -182,11 +183,12 @@ class Nation : RulesetObject() { } private fun getUniqueBuildingsText(ruleset: Ruleset) = sequence { + val religionEnabled = showReligionInCivilopedia(ruleset) for (building in ruleset.buildings.values) { when { building.uniqueTo != name -> continue building.hasUnique(UniqueType.HiddenFromCivilopedia) -> continue - UncivGame.Current.gameInfo != null && !UncivGame.Current.gameInfo!!.isReligionEnabled() && building.hasUnique(UniqueType.HiddenWithoutReligion) -> continue // This seems consistent with existing behaviour of CivilopediaScreen's init..shouldBeDisplayed(), and Technology().getEnabledUnits(). Otherwise there are broken links in the Civilopedia (E.G. to "Pyramid" and "Shrine", from "The Maya"). + religionEnabled && building.hasUnique(UniqueType.HiddenWithoutReligion) -> continue } yield(FormattedLine("{${building.name}} -", link=building.makeLink())) if (building.replaces != null && ruleset.buildings.containsKey(building.replaces!!)) { diff --git a/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt b/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt index adc56c8d05..69a19ef16d 100644 --- a/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt +++ b/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt @@ -11,7 +11,9 @@ import com.unciv.models.ruleset.RulesetStatsObject import com.unciv.models.ruleset.unique.StateForConditionals 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.civilopedia.CivilopediaScreen.Companion.showReligionInCivilopedia import com.unciv.ui.civilopedia.FormattedLine import com.unciv.ui.utils.extensions.toPercent import com.unciv.ui.worldscreen.unit.UnitActions @@ -131,12 +133,16 @@ class TileImprovement : RulesetStatsObject() { val statsDesc = cloneStats().toString() if (statsDesc.isNotEmpty()) textList += FormattedLine(statsDesc) - if (uniqueTo!=null) { + if (uniqueTo != null) { textList += FormattedLine() textList += FormattedLine("Unique to [$uniqueTo]", link="Nation/$uniqueTo") } - if (terrainsCanBeBuiltOn.isNotEmpty()) { + 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()) { @@ -151,15 +157,15 @@ class TileImprovement : RulesetStatsObject() { } var addedLineBeforeResourceBonus = false - for (resource in ruleset.tileResources.values.filter { it.isImprovedBy(name) }) { - if (resource.improvementStats == null) continue + 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() - - textList += FormattedLine("[${statsString}] ", link = "Resource/${resource.name}") + // Line intentionally modeled as UniqueType.Stats + ConditionalInTiles + textList += FormattedLine("[${statsString}] ", link = resource.makeLink()) } if (techRequired != null) { @@ -173,16 +179,21 @@ class TileImprovement : RulesetStatsObject() { textList += FormattedLine(unique) } + // 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: String - val religionEnabled: Boolean - if (UncivGame.isCurrentInitialized() && UncivGame.Current.gameInfo != null) { - difficulty = UncivGame.Current.gameInfo!!.gameParameters.difficulty - religionEnabled = UncivGame.Current.gameInfo!!.isReligionEnabled() - } else { - difficulty = "Prince" // most factors == 1 - religionEnabled = true - } + 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() @@ -196,18 +207,75 @@ class TileImprovement : RulesetStatsObject() { } } - val unit = ruleset.units.asSequence().firstOrNull { - entry -> entry.value.uniques.any { - it.startsWith("Can construct [$name]") - } - }?.key - if (unit != null) { + if (creatorExists) textList += FormattedLine() - textList += FormattedLine("{Can be constructed by} {$unit}", link="Unit/$unit") - } + 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 { + //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 + // `matchesFilter("Land")==true` - but that's not the case. + // A kludge, but for display purposes the test below is meaningful enough. + if (hasUnique(UniqueType.Unbuildable)) return emptyList() + + val canOnlyFilters = getMatchingUniques(UniqueType.CanOnlyBeBuiltOnTile) + .map { it.params[0].run { if (this == "Coastal") "Land" else this } }.toSet() + val cannotFilters = getMatchingUniques(UniqueType.CannotBuildOnTile).map { it.params[0] }.toSet() + val resourcesImprovedByThis = ruleset.tileResources.values.filter { it.isImprovedBy(name) } + + val expandedCanBeBuiltOn = sequence { + yieldAll(terrainsCanBeBuiltOn) + yieldAll(terrainsCanBeBuiltOn.asSequence().mapNotNull { ruleset.terrains[it] }.flatMap { it.occursOn.asSequence() }) + if (hasUnique(UniqueType.CanOnlyImproveResource)) + yieldAll(resourcesImprovedByThis.asSequence().flatMap { it.terrainsCanBeFoundOn }) + if (name.startsWith(Constants.remove)) name.removePrefix(Constants.remove).apply { + yield(this) + ruleset.terrains[this]?.occursOn?.let { yieldAll(it) } + ruleset.tileImprovements[this]?.terrainsCanBeBuiltOn?.let { yieldAll(it) } + } + }.filter { it !in cannotFilters }.toMutableSet() + + val terrainsCanBeBuiltOnTypes = sequence { + yieldAll(expandedCanBeBuiltOn.asSequence() + .mapNotNull { ruleset.terrains[it]?.type }) + yieldAll(TerrainType.values().asSequence() + .filter { it.name in expandedCanBeBuiltOn }) + }.filter { it.name !in cannotFilters }.toMutableSet() + + if (canOnlyFilters.isNotEmpty() && canOnlyFilters.intersect(expandedCanBeBuiltOn).isEmpty()) { + expandedCanBeBuiltOn.clear() + if (terrainsCanBeBuiltOnTypes.none { it.name in canOnlyFilters }) + terrainsCanBeBuiltOnTypes.clear() + } + + fun matchesBuildImprovementsFilter(filter: String) = + matchesFilter(filter) || + filter in expandedCanBeBuiltOn || + terrainsCanBeBuiltOnTypes.any { it.name == filter } + + return ruleset.units.values.asSequence() + .filter { unit -> + turnsToBuild != 0 + && unit.getMatchingUniques(UniqueType.BuildImprovements, StateForConditionals.IgnoreConditionals) + .any { matchesBuildImprovementsFilter(it.params[0]) } + || unit.hasUnique(UniqueType.CreateWaterImprovements) + && terrainsCanBeBuiltOnTypes.contains(TerrainType.Water) + }.toList() + } + + private fun getCreatingUnits(ruleset: Ruleset): List { + return ruleset.units.values.asSequence() + .filter { unit -> + unit.getMatchingUniques(UniqueType.ConstructImprovementConsumingUnit, StateForConditionals.IgnoreConditionals) + .any { it.params[0] == name } + }.toList() + } } diff --git a/core/src/com/unciv/models/ruleset/unique/Unique.kt b/core/src/com/unciv/models/ruleset/unique/Unique.kt index 409ed60e58..4f6df03060 100644 --- a/core/src/com/unciv/models/ruleset/unique/Unique.kt +++ b/core/src/com/unciv/models/ruleset/unique/Unique.kt @@ -197,6 +197,9 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s state.ourCombatant != null && state.ourCombatant.getHealth() > condition.params[0].toInt() UniqueType.ConditionalBelowHP -> state.ourCombatant != null && state.ourCombatant.getHealth() < condition.params[0].toInt() + UniqueType.ConditionalHasNotUsedOtherActions -> + state.unit != null && + state.unit.run { religiousActionsUnitCanDo().all { abilityUsesLeft[it] == maxAbilityUses[it] } } UniqueType.ConditionalInTiles -> relevantTile?.matchesFilter(condition.params[0], state.civInfo) == true diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index 8e4742e4e1..7aa718ca38 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -405,6 +405,7 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags: FoundCity("Founds a new city", UniqueTarget.Unit), ConstructImprovementConsumingUnit("Can construct [improvementName]", UniqueTarget.Unit), + @Deprecated("as of 4.1.7", ReplaceWith("Can construct [improvementName] ")) CanConstructIfNoOtherActions("Can construct [improvementName] if it hasn't used other actions yet", UniqueTarget.Unit), BuildImprovements("Can build [improvementFilter/terrainFilter] improvements on tiles", UniqueTarget.Unit), CreateWaterImprovements("May create improvements on water resources", UniqueTarget.Unit), @@ -697,6 +698,7 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags: ConditionalAdjacentUnit("when adjacent to a [mapUnitFilter] unit", UniqueTarget.Conditional), ConditionalAboveHP("when above [amount] HP", UniqueTarget.Conditional), ConditionalBelowHP("when below [amount] HP", UniqueTarget.Conditional), + ConditionalHasNotUsedOtherActions("if it hasn't used other actions yet", UniqueTarget.Conditional), /////// tile conditionals ConditionalNeighborTiles("with [amount] to [amount] neighboring [tileFilter] tiles", UniqueTarget.Conditional), @@ -1075,4 +1077,3 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags: // I didn't put this is a companion object because APPARENTLY doing that means you can't use it in the init function. val numberRegex = Regex("\\d+$") // Any number of trailing digits - diff --git a/core/src/com/unciv/ui/civilopedia/CivilopediaCategories.kt b/core/src/com/unciv/ui/civilopedia/CivilopediaCategories.kt index f4c25f803b..f40514770d 100644 --- a/core/src/com/unciv/ui/civilopedia/CivilopediaCategories.kt +++ b/core/src/com/unciv/ui/civilopedia/CivilopediaCategories.kt @@ -16,6 +16,7 @@ import com.unciv.ui.utils.KeyCharAndCode import com.unciv.ui.utils.extensions.surroundWithCircle import java.io.File + /** Encapsulates the knowledge on how to get an icon for each of the Civilopedia categories */ object CivilopediaImageGetters { private const val policyIconFolder = "PolicyIcons" diff --git a/core/src/com/unciv/ui/civilopedia/CivilopediaScreen.kt b/core/src/com/unciv/ui/civilopedia/CivilopediaScreen.kt index 7ea8a830e2..52022a04ea 100644 --- a/core/src/com/unciv/ui/civilopedia/CivilopediaScreen.kt +++ b/core/src/com/unciv/ui/civilopedia/CivilopediaScreen.kt @@ -183,9 +183,8 @@ class CivilopediaScreen( val imageSize = 50f globalShortcuts.add(KeyCharAndCode.BACK) { game.popScreen() } - val curGameInfo = game.gameInfo - val religionEnabled = if (curGameInfo != null) curGameInfo.isReligionEnabled() else ruleset.beliefs.isNotEmpty() - val victoryTypes = if (curGameInfo != null) curGameInfo.gameParameters.victoryTypes else ruleset.victories.keys + val religionEnabled = showReligionInCivilopedia(ruleset) + val victoryTypes = game.gameInfo?.gameParameters?.victoryTypes ?: ruleset.victories.keys fun shouldBeDisplayed(obj: IHasUniques): Boolean { return when { @@ -319,4 +318,15 @@ class CivilopediaScreen( } override fun recreate(): BaseScreen = CivilopediaScreen(ruleset, currentCategory, currentEntry) + + companion object { + /** Test whether to show Religion-specific items, does not require a game to be running */ + // Here we decide whether to show Religion in Civilopedia from Main Menu (no gameInfo loaded) + fun showReligionInCivilopedia(ruleset: Ruleset? = null) = when { + UncivGame.isCurrentInitialized() && UncivGame.Current.gameInfo != null -> + UncivGame.Current.gameInfo!!.isReligionEnabled() + ruleset != null -> ruleset.beliefs.isNotEmpty() + else -> true + } + } } diff --git a/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt b/core/src/com/unciv/ui/civilopedia/FormattedLine.kt similarity index 66% rename from core/src/com/unciv/ui/civilopedia/CivilopediaText.kt rename to core/src/com/unciv/ui/civilopedia/FormattedLine.kt index fd3dab402e..f40118fe8b 100644 --- a/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt +++ b/core/src/com/unciv/ui/civilopedia/FormattedLine.kt @@ -11,12 +11,8 @@ import com.unciv.models.metadata.BaseRuleset import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.unique.Unique -import com.unciv.models.stats.INamed -import com.unciv.ui.civilopedia.MarkupRenderer.render import com.unciv.ui.images.ImageGetter import com.unciv.ui.utils.BaseScreen -import com.unciv.ui.utils.extensions.addSeparator -import com.unciv.ui.utils.extensions.onClick import com.unciv.ui.utils.extensions.toLabel import com.unciv.utils.Log import kotlin.math.max @@ -164,13 +160,10 @@ class FormattedLine ( } return "" } - private fun getCurrentRuleset(): Ruleset { - val gameInfo = UncivGame.Current.gameInfo - return when { - !UncivGame.isCurrentInitialized() -> Ruleset() - gameInfo == null -> RulesetCache[BaseRuleset.Civ_V_Vanilla.fullName]!! - else -> gameInfo.ruleSet - } + private fun getCurrentRuleset() = when { + !UncivGame.isCurrentInitialized() -> Ruleset() + UncivGame.Current.gameInfo == null -> RulesetCache[BaseRuleset.Civ_V_Vanilla.fullName]!! + else -> UncivGame.Current.gameInfo!!.ruleSet } private fun initNamesCategoryMap(ruleSet: Ruleset): HashMap { //val startTime = System.nanoTime() @@ -341,165 +334,3 @@ class FormattedLine ( } } } - -/** Makes [renderer][render] available outside [ICivilopediaText] */ -object MarkupRenderer { - /** Height of empty line (`FormattedLine()`) - about half a normal text line, independent of font size */ - private const val emptyLineHeight = 10f - /** Default cell padding of non-empty lines */ - private const val defaultPadding = 2.5f - /** Padding above a [separator][FormattedLine.separator] line */ - private const val separatorTopPadding = 10f - /** Padding below a [separator][FormattedLine.separator] line */ - private const val separatorBottomPadding = 10f - - /** - * Build a Gdx [Table] showing [formatted][FormattedLine] [content][lines]. - * - * @param labelWidth Available width needed for wrapping labels and [centered][FormattedLine.centered] attribute. - * @param padding Default cell padding (default 2.5f) to control line spacing - * @param iconDisplay Flag to omit link or all images (but not linking itself if linkAction is supplied) - * @param linkAction Delegate to call for internal links. Leave null to suppress linking. - */ - fun render( - lines: Collection, - labelWidth: Float = 0f, - padding: Float = defaultPadding, - iconDisplay: FormattedLine.IconDisplay = FormattedLine.IconDisplay.All, - linkAction: ((id: String) -> Unit)? = null - ): Table { - val skin = BaseScreen.skin - val table = Table(skin).apply { defaults().pad(padding).align(Align.left) } - for (line in lines) { - if (line.isEmpty()) { - table.add().padTop(emptyLineHeight).row() - continue - } - if (line.separator) { - table.addSeparator(line.displayColor, 1, if (line.size == Int.MIN_VALUE) 2f else line.size.toFloat()) - .pad(separatorTopPadding, 0f, separatorBottomPadding, 0f) - continue - } - val actor = line.render(labelWidth, iconDisplay) - if (line.linkType == FormattedLine.LinkType.Internal && linkAction != null) - actor.onClick { - linkAction(line.link) - } - else if (line.linkType == FormattedLine.LinkType.External) - actor.onClick { - Gdx.net.openURI(line.link) - } - if (labelWidth == 0f) - table.add(actor).align(line.align).row() - else - table.add(actor).width(labelWidth).align(line.align).row() - } - return table.apply { pack() } - } -} - -/** Storage class for instantiation of the simplest form containing only the lines collection */ -open class SimpleCivilopediaText( - override var civilopediaText: List -) : ICivilopediaText { - constructor(strings: Sequence) : this( - strings.map { FormattedLine(it) }.toList()) - constructor(first: Sequence, strings: Sequence) : this( - (first + strings.map { FormattedLine(it) }).toList()) - - override fun makeLink() = "" -} - -/** Addon common to most ruleset game objects managing civilopedia display - * - * ### Usage: - * 1. Let [Ruleset] object implement this (by inheriting and implementing class [ICivilopediaText]) - * 2. Add `"civilopediaText": ["",…],` in the json for these objects - * 3. Optionally override [getCivilopediaTextHeader] to supply a different header line - * 4. Optionally override [getCivilopediaTextLines] to supply automatic stuff like tech prerequisites, uniques, etc. - * 4. Optionally override [assembleCivilopediaText] to handle assembly of the final set of lines yourself. - */ -interface ICivilopediaText { - /** List of strings supporting simple [formatting rules][FormattedLine] that [CivilopediaScreen] can render. - * May later be merged with automatic lines generated by the deriving class - * through overridden [getCivilopediaTextHeader] and/or [getCivilopediaTextLines] methods. - */ - var civilopediaText: List - - /** Generate header line from object metadata. - * Default implementation will take [INamed.name] and render it in 150% normal font size with an icon from [makeLink]. - * @return A [FormattedLine] that will be inserted on top - */ - fun getCivilopediaTextHeader(): FormattedLine? = - if (this is INamed) FormattedLine(name, icon = makeLink(), header = 2) - else null - - /** Generate automatic lines from object metadata. - * - * This function ***MUST not rely*** on [UncivGame.Current.gameInfo][UncivGame.gameInfo] - * **or** [UncivGame.Current.worldScreen][UncivGame.worldScreen] being initialized, - * this should be able to run from the main menu. - * (And the info displayed should be about the **ruleset**, not the player situation) - * - * Default implementation is empty - no need to call super in overrides. - * - * @param ruleset The current ruleset for the Civilopedia viewer - * @return A list of [FormattedLine]s that will be inserted before - * the first line of [civilopediaText] having a [link][FormattedLine.link] - */ - fun getCivilopediaTextLines(ruleset: Ruleset): List = listOf() - - /** Build a Gdx [Table] showing our [formatted][FormattedLine] [content][civilopediaText]. */ - fun renderCivilopediaText (labelWidth: Float, linkAction: ((id: String)->Unit)? = null): Table { - return MarkupRenderer.render(civilopediaText, labelWidth, linkAction = linkAction) - } - - /** Assemble json-supplied lines with automatically generated ones. - * - * The default implementation will insert [getCivilopediaTextLines] before the first [linked][FormattedLine.link] [civilopediaText] line and [getCivilopediaTextHeader] on top. - * - * @param ruleset The current ruleset for the Civilopedia viewer - * @return A new CivilopediaText instance containing original [civilopediaText] lines merged with those from [getCivilopediaTextHeader] and [getCivilopediaTextLines] calls. - */ - fun assembleCivilopediaText(ruleset: Ruleset): ICivilopediaText { - val outerLines = civilopediaText.iterator() - val newLines = sequence { - var middleDone = false - var outerNotEmpty = false - val header = getCivilopediaTextHeader() - if (header != null) { - yield(header) - yield(FormattedLine(separator = true)) - } - while (outerLines.hasNext()) { - val next = outerLines.next() - if (!middleDone && !next.isEmpty() && next.linkType != FormattedLine.LinkType.None) { - middleDone = true - if (outerNotEmpty) yield(FormattedLine()) - yieldAll(getCivilopediaTextLines(ruleset)) - yield(FormattedLine()) - } - outerNotEmpty = true - yield(next) - } - if (!middleDone) { - if (outerNotEmpty) yield(FormattedLine()) - yieldAll(getCivilopediaTextLines(ruleset)) - } - } - return SimpleCivilopediaText(newLines.toList()) - } - - /** Create the correct string for a Civilopedia link */ - fun makeLink(): String - - /** Overrides alphabetical sorting in Civilopedia - * @param ruleset The current ruleset in case the function needs to do lookups - */ - fun getSortGroup(ruleset: Ruleset): Int = 0 - - /** Overrides Icon used for Civilopedia entry list (where you select the instance) - * This will still be passed to the category-specific image getter. - */ - fun getIconName() = if (this is INamed) name else "" -} diff --git a/core/src/com/unciv/ui/civilopedia/ICivilopediaText.kt b/core/src/com/unciv/ui/civilopedia/ICivilopediaText.kt new file mode 100644 index 0000000000..310d4cf6a2 --- /dev/null +++ b/core/src/com/unciv/ui/civilopedia/ICivilopediaText.kt @@ -0,0 +1,101 @@ +package com.unciv.ui.civilopedia + +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.unciv.models.ruleset.Ruleset +import com.unciv.models.stats.INamed +import com.unciv.UncivGame // Kdoc only + + +/** Addon common to most ruleset game objects managing civilopedia display + * + * ### Usage: + * 1. Let [Ruleset] object implement this (by inheriting and implementing class [ICivilopediaText]) + * 2. Add `"civilopediaText": ["",…],` in the json for these objects + * 3. Optionally override [getCivilopediaTextHeader] to supply a different header line + * 4. Optionally override [getCivilopediaTextLines] to supply automatic stuff like tech prerequisites, uniques, etc. + * 4. Optionally override [assembleCivilopediaText] to handle assembly of the final set of lines yourself. + */ +interface ICivilopediaText { + /** List of strings supporting simple [formatting rules][FormattedLine] that [CivilopediaScreen] can render. + * May later be merged with automatic lines generated by the deriving class + * through overridden [getCivilopediaTextHeader] and/or [getCivilopediaTextLines] methods. + */ + var civilopediaText: List + + /** Generate header line from object metadata. + * Default implementation will take [INamed.name] and render it in 150% normal font size with an icon from [makeLink]. + * @return A [FormattedLine] that will be inserted on top + */ + fun getCivilopediaTextHeader(): FormattedLine? = + if (this is INamed) FormattedLine(name, icon = makeLink(), header = 2) + else null + + /** Generate automatic lines from object metadata. + * + * This function ***MUST not rely*** on [UncivGame.Current.gameInfo][UncivGame.gameInfo] + * **or** [UncivGame.Current.worldScreen][UncivGame.worldScreen] being initialized, + * this should be able to run from the main menu. + * (And the info displayed should be about the **ruleset**, not the player situation) + * + * Default implementation is empty - no need to call super in overrides. + * + * @param ruleset The current ruleset for the Civilopedia viewer + * @return A list of [FormattedLine]s that will be inserted before + * the first line of [civilopediaText] having a [link][FormattedLine.link] + */ + fun getCivilopediaTextLines(ruleset: Ruleset): List = listOf() + + /** Build a Gdx [Table] showing our [formatted][FormattedLine] [content][civilopediaText]. */ + fun renderCivilopediaText (labelWidth: Float, linkAction: ((id: String)->Unit)? = null): Table { + return MarkupRenderer.render(civilopediaText, labelWidth, linkAction = linkAction) + } + + /** Assemble json-supplied lines with automatically generated ones. + * + * The default implementation will insert [getCivilopediaTextLines] before the first [linked][FormattedLine.link] [civilopediaText] line and [getCivilopediaTextHeader] on top. + * + * @param ruleset The current ruleset for the Civilopedia viewer + * @return A new CivilopediaText instance containing original [civilopediaText] lines merged with those from [getCivilopediaTextHeader] and [getCivilopediaTextLines] calls. + */ + fun assembleCivilopediaText(ruleset: Ruleset): ICivilopediaText { + val outerLines = civilopediaText.iterator() + val newLines = sequence { + var middleDone = false + var outerNotEmpty = false + val header = getCivilopediaTextHeader() + if (header != null) { + yield(header) + yield(FormattedLine(separator = true)) + } + while (outerLines.hasNext()) { + val next = outerLines.next() + if (!middleDone && !next.isEmpty() && next.linkType != FormattedLine.LinkType.None) { + middleDone = true + if (outerNotEmpty) yield(FormattedLine()) + yieldAll(getCivilopediaTextLines(ruleset)) + yield(FormattedLine()) + } + outerNotEmpty = true + yield(next) + } + if (!middleDone) { + if (outerNotEmpty) yield(FormattedLine()) + yieldAll(getCivilopediaTextLines(ruleset)) + } + } + return SimpleCivilopediaText(newLines.toList()) + } + + /** Create the correct string for a Civilopedia link */ + fun makeLink(): String + + /** Overrides alphabetical sorting in Civilopedia + * @param ruleset The current ruleset in case the function needs to do lookups + */ + fun getSortGroup(ruleset: Ruleset): Int = 0 + + /** Overrides Icon used for Civilopedia entry list (where you select the instance) + * This will still be passed to the category-specific image getter. + */ + fun getIconName() = if (this is INamed) name else "" +} diff --git a/core/src/com/unciv/ui/civilopedia/MarkupRenderer.kt b/core/src/com/unciv/ui/civilopedia/MarkupRenderer.kt new file mode 100644 index 0000000000..50dcc38015 --- /dev/null +++ b/core/src/com/unciv/ui/civilopedia/MarkupRenderer.kt @@ -0,0 +1,65 @@ +package com.unciv.ui.civilopedia + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.badlogic.gdx.utils.Align +import com.unciv.ui.utils.BaseScreen +import com.unciv.ui.utils.extensions.addSeparator +import com.unciv.ui.utils.extensions.onClick + + +/** Makes [renderer][render] available outside [ICivilopediaText] */ +object MarkupRenderer { + /** Height of empty line (`FormattedLine()`) - about half a normal text line, independent of font size */ + private const val emptyLineHeight = 10f + /** Default cell padding of non-empty lines */ + private const val defaultPadding = 2.5f + /** Padding above a [separator][FormattedLine.separator] line */ + private const val separatorTopPadding = 10f + /** Padding below a [separator][FormattedLine.separator] line */ + private const val separatorBottomPadding = 10f + + /** + * Build a Gdx [Table] showing [formatted][FormattedLine] [content][lines]. + * + * @param labelWidth Available width needed for wrapping labels and [centered][FormattedLine.centered] attribute. + * @param padding Default cell padding (default 2.5f) to control line spacing + * @param iconDisplay Flag to omit link or all images (but not linking itself if linkAction is supplied) + * @param linkAction Delegate to call for internal links. Leave null to suppress linking. + */ + fun render( + lines: Collection, + labelWidth: Float = 0f, + padding: Float = defaultPadding, + iconDisplay: FormattedLine.IconDisplay = FormattedLine.IconDisplay.All, + linkAction: ((id: String) -> Unit)? = null + ): Table { + val skin = BaseScreen.skin + val table = Table(skin).apply { defaults().pad(padding).align(Align.left) } + for (line in lines) { + if (line.isEmpty()) { + table.add().padTop(emptyLineHeight).row() + continue + } + if (line.separator) { + table.addSeparator(line.displayColor, 1, if (line.size == Int.MIN_VALUE) 2f else line.size.toFloat()) + .pad(separatorTopPadding, 0f, separatorBottomPadding, 0f) + continue + } + val actor = line.render(labelWidth, iconDisplay) + if (line.linkType == FormattedLine.LinkType.Internal && linkAction != null) + actor.onClick { + linkAction(line.link) + } + else if (line.linkType == FormattedLine.LinkType.External) + actor.onClick { + Gdx.net.openURI(line.link) + } + if (labelWidth == 0f) + table.add(actor).align(line.align).row() + else + table.add(actor).width(labelWidth).align(line.align).row() + } + return table.apply { pack() } + } +} diff --git a/core/src/com/unciv/ui/civilopedia/SimpleCivilopediaText.kt b/core/src/com/unciv/ui/civilopedia/SimpleCivilopediaText.kt new file mode 100644 index 0000000000..463c215d88 --- /dev/null +++ b/core/src/com/unciv/ui/civilopedia/SimpleCivilopediaText.kt @@ -0,0 +1,13 @@ +package com.unciv.ui.civilopedia + +/** Storage class for instantiation of the simplest form containing only the lines collection */ +open class SimpleCivilopediaText( + override var civilopediaText: List +) : ICivilopediaText { + constructor(strings: Sequence) : this( + strings.map { FormattedLine(it) }.toList()) + constructor(first: Sequence, strings: Sequence) : this( + (first + strings.map { FormattedLine(it) }).toList()) + + override fun makeLink() = "" +}