mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-09 23:39:40 +07:00
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:
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 } }
|
||||
}
|
||||
|
@ -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!!)) {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 ""
|
||||
}
|
101
core/src/com/unciv/ui/civilopedia/ICivilopediaText.kt
Normal file
101
core/src/com/unciv/ui/civilopedia/ICivilopediaText.kt
Normal 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 ""
|
||||
}
|
65
core/src/com/unciv/ui/civilopedia/MarkupRenderer.kt
Normal file
65
core/src/com/unciv/ui/civilopedia/MarkupRenderer.kt
Normal 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() }
|
||||
}
|
||||
}
|
13
core/src/com/unciv/ui/civilopedia/SimpleCivilopediaText.kt
Normal file
13
core/src/com/unciv/ui/civilopedia/SimpleCivilopediaText.kt
Normal 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() = ""
|
||||
}
|
Reference in New Issue
Block a user