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"
This commit is contained in:
SomeTroglodyte
2022-06-19 02:29:07 +02:00
committed by GitHub
parent 672b804ac5
commit 9683e27526
14 changed files with 313 additions and 217 deletions

View File

@ -1623,7 +1623,7 @@
{
"name": "Great Prophet",
"unitType": "Civilian",
"uniques": ["Can construct [Holy site] if it hasn't used other actions yet", "Can [Spread Religion] [4] times",
"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",

View File

@ -1299,7 +1299,7 @@
{
"name": "Great Prophet",
"unitType": "Civilian",
"uniques": ["Can construct [Holy site] if it hasn't used other actions yet", "Can [Spread Religion] [4] times",
"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",

View File

@ -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

View File

@ -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<Belief> {
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 } }
}

View File

@ -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.<locals>.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!!)) {

View File

@ -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}] <in [${resource.name}] tiles>", link = "Resource/${resource.name}")
// Line intentionally modeled as UniqueType.Stats + ConditionalInTiles
textList += FormattedLine("[${statsString}] <in [${resource.name}] tiles>", 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<BaseUnit> {
//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<BaseUnit> {
return ruleset.units.values.asSequence()
.filter { unit ->
unit.getMatchingUniques(UniqueType.ConstructImprovementConsumingUnit, StateForConditionals.IgnoreConditionals)
.any { it.params[0] == name }
}.toList()
}
}

View File

@ -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

View File

@ -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] <if it hasn't used other actions yet>"))
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

View File

@ -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"

View File

@ -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
}
}
}

View File

@ -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 {
private fun getCurrentRuleset() = when {
!UncivGame.isCurrentInitialized() -> Ruleset()
gameInfo == null -> RulesetCache[BaseRuleset.Civ_V_Vanilla.fullName]!!
else -> gameInfo.ruleSet
}
UncivGame.Current.gameInfo == null -> RulesetCache[BaseRuleset.Civ_V_Vanilla.fullName]!!
else -> UncivGame.Current.gameInfo!!.ruleSet
}
private fun initNamesCategoryMap(ruleSet: Ruleset): HashMap<String, CivilopediaCategories> {
//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<FormattedLine>,
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<FormattedLine>
) : ICivilopediaText {
constructor(strings: Sequence<String>) : this(
strings.map { FormattedLine(it) }.toList())
constructor(first: Sequence<FormattedLine>, strings: Sequence<String>) : 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<FormattedLine>
/** 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<FormattedLine> = 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 ""
}

View File

@ -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<FormattedLine>
/** 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<FormattedLine> = 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 ""
}

View File

@ -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<FormattedLine>,
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() }
}
}

View File

@ -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<FormattedLine>
) : ICivilopediaText {
constructor(strings: Sequence<String>) : this(
strings.map { FormattedLine(it) }.toList())
constructor(first: Sequence<FormattedLine>, strings: Sequence<String>) : this(
(first + strings.map { FormattedLine(it) }).toList())
override fun makeLink() = ""
}