Spruced up Civilopedia - phase 4 - Visual candy, Units (#4350)

* Spruced up Civilopedia - phase 4 - Visual candy, Units

* Unified separators, CheckBox helper - patch2

* Unified separators, CheckBox helper - atlas merge

* Spruced up Civilopedia - phase 4 - rebuild atlas

* Spruced up Civilopedia - phase 4 - rebuild atlas

* Spruced up Civilopedia - phase 4 - do separator to-do
This commit is contained in:
SomeTroglodyte 2021-07-05 15:35:41 +02:00 committed by GitHub
parent c2a43ffee0
commit c42561c545
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 943 additions and 706 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -37,7 +37,10 @@
"cost": 40, "cost": 40,
"obsoleteTech": "Metal Casting", "obsoleteTech": "Metal Casting",
"upgradesTo": "Swordsman", "upgradesTo": "Swordsman",
"attackSound": "nonmetalhit" "attackSound": "nonmetalhit",
"civilopediaText": [
{"text": "This is your basic, club-swinging fighter."}
]
}, },
{ {
"name": "Maori Warrior", "name": "Maori Warrior",

View File

@ -174,7 +174,7 @@ class Nation : INamed {
textList += unit.name.tr() + " - " + "Replaces [${unit.replaces}], which is not found in the ruleset!".tr() textList += unit.name.tr() + " - " + "Replaces [${unit.replaces}], which is not found in the ruleset!".tr()
} else { } else {
textList += unit.name.tr() textList += unit.name.tr()
textList += " " + unit.getDescription(true).split("\n").joinToString("\n ") textList += " " + unit.getDescription().split("\n").joinToString("\n ")
} }
textList += "" textList += ""

View File

@ -7,8 +7,10 @@ import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.MapUnit import com.unciv.logic.map.MapUnit
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.Unique import com.unciv.models.ruleset.Unique
import com.unciv.models.stats.INamed
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.models.stats.INamed
import com.unciv.ui.civilopedia.CivilopediaText
import com.unciv.ui.civilopedia.FormattedLine
import com.unciv.ui.utils.Fonts import com.unciv.ui.utils.Fonts
import kotlin.math.pow import kotlin.math.pow
@ -16,7 +18,7 @@ import kotlin.math.pow
/** This is the basic info of the units, as specified in Units.json, /** This is the basic info of the units, as specified in Units.json,
in contrast to MapUnit, which is a specific unit of a certain type that appears on the map */ in contrast to MapUnit, which is a specific unit of a certain type that appears on the map */
class BaseUnit : INamed, IConstruction { class BaseUnit : INamed, IConstruction, CivilopediaText() {
override lateinit var name: String override lateinit var name: String
var cost: Int = 0 var cost: Int = 0
@ -52,36 +54,115 @@ class BaseUnit : INamed, IConstruction {
return infoList.joinToString() return infoList.joinToString()
} }
fun getDescription(forPickerScreen: Boolean): String { /** Generate description as multi-line string for Nation description addUniqueUnitsText and CityScreen addSelectedConstructionTable */
val sb = StringBuilder() fun getDescription(): String {
val lines = mutableListOf<String>()
for ((resource, amount) in getResourceRequirements()) { for ((resource, amount) in getResourceRequirements()) {
if (amount == 1) sb.appendLine("Consumes 1 [$resource]".tr()) lines += if (amount == 1) "Consumes 1 [$resource]".tr()
else sb.appendLine("Consumes [$amount]] [$resource]".tr()) else "Consumes [$amount] [$resource]".tr()
}
if (!forPickerScreen) {
if (uniqueTo != null) sb.appendLine("Unique to [$uniqueTo], replaces [$replaces]".tr())
else sb.appendLine("{Cost}: $cost".tr())
if (requiredTech != null) sb.appendLine("Required tech: [$requiredTech]".tr())
if (upgradesTo != null) sb.appendLine("Upgrades to [$upgradesTo]".tr())
if (obsoleteTech != null) sb.appendLine("Obsolete with [$obsoleteTech]".tr())
} }
var strengthLine = ""
if (strength != 0) { if (strength != 0) {
sb.append("$strength${Fonts.strength}, ") strengthLine += "$strength${Fonts.strength}, "
if (rangedStrength != 0) sb.append("$rangedStrength${Fonts.rangedStrength}, ") if (rangedStrength != 0)
if (rangedStrength != 0) sb.append("$range${Fonts.range}, ") strengthLine += "$rangedStrength${Fonts.rangedStrength}, $range${Fonts.range}, "
} }
sb.appendLine("$movement${Fonts.movement}") lines += "$strengthLine$movement${Fonts.movement}"
if (replacementTextForUniques != "") sb.appendLine(replacementTextForUniques) if (replacementTextForUniques != "") lines += replacementTextForUniques
else for (unique in uniques) else for (unique in uniques)
sb.appendLine(unique.tr()) lines += unique.tr()
if (promotions.isNotEmpty()) { if (promotions.isNotEmpty()) {
sb.append((if (promotions.size == 1) "Free promotion:" else "Free promotions:").tr()) val prefix = "Free promotion${if (promotions.size == 1) "" else "s"}:".tr() + " "
sb.appendLine(promotions.joinToString(", ", " ") { it.tr() }) lines += promotions.joinToString(", ", prefix) { it.tr() }
} }
return sb.toString().trim() return lines.joinToString("\n")
}
override fun getCivilopediaTextHeader() = FormattedLine(name, icon="Unit/$name", header=2)
override fun replacesCivilopediaDescription() = true
override fun hasCivilopediaTextLines() = true
override fun getCivilopediaTextLines(ruleset: Ruleset): List<FormattedLine> {
val textList = ArrayList<FormattedLine>()
val stats = ArrayList<String>()
if (strength != 0) stats += "$strength${Fonts.strength}"
if (rangedStrength != 0) {
stats += "$rangedStrength${Fonts.rangedStrength}"
stats += "$range${Fonts.range}"
}
if (movement != 0) stats += "$movement${Fonts.movement}"
if (cost != 0) stats += "{Cost}: $cost"
if (stats.isNotEmpty())
textList += FormattedLine(stats.joinToString(", "))
if (replacementTextForUniques != "") {
textList += FormattedLine()
textList += FormattedLine(replacementTextForUniques)
} else if (uniques.isNotEmpty()) {
textList += FormattedLine()
for (uniqueObject in uniqueObjects.sortedBy { it.text }) {
if (uniqueObject.placeholderText == "Can construct []") {
val improvement = uniqueObject.params[0]
textList += FormattedLine(uniqueObject.text, link="Improvement/$improvement")
} else {
textList += FormattedLine(uniqueObject.text)
}
}
}
val resourceRequirements = getResourceRequirements()
if (resourceRequirements.isNotEmpty()) {
textList += FormattedLine()
for ((resource, amount) in resourceRequirements) {
textList += FormattedLine(
if (amount == 1) "Consumes 1 [$resource]" else "Consumes [$amount] [$resource]",
link="Resource/$resource", color="#F42")
}
}
if (uniqueTo != null) {
textList += FormattedLine()
textList += FormattedLine("Unique to [$uniqueTo],", link="Nation/$uniqueTo")
if (replaces != null)
textList += FormattedLine("replaces [$replaces]", link="Unit/$replaces", indent=1)
}
if (requiredTech != null || upgradesTo != null || obsoleteTech != null) textList += FormattedLine()
if (requiredTech != null) textList += FormattedLine("Required tech: [$requiredTech]", link="Technology/$requiredTech")
if (upgradesTo != null) textList += FormattedLine("Upgrades to [$upgradesTo]", link="Unit/$upgradesTo")
if (obsoleteTech != null) textList += FormattedLine("Obsolete with [$obsoleteTech]", link="Technology/$obsoleteTech")
if (promotions.isNotEmpty()) {
textList += FormattedLine()
promotions.withIndex().forEach {
textList += FormattedLine(
when {
promotions.size == 1 -> "{Free promotion:} "
it.index == 0 -> "{Free promotions:} "
else -> ""
} + "{${it.value}}" +
(if (promotions.size == 1 || it.index == promotions.size - 1) "" else ","),
link="Promotions/${it.value}",
indent=if(it.index==0) 0 else 1)
}
}
val seeAlso = ArrayList<FormattedLine>()
for ((other, unit) in ruleset.units) {
if (unit.replaces == name || uniques.contains("[$name]") ) {
seeAlso += FormattedLine(other, link="Unit/$other", indent=1)
}
}
if (seeAlso.isNotEmpty()) {
textList += FormattedLine()
textList += FormattedLine("{See also}:")
textList += seeAlso
}
return textList
} }
fun getMapUnit(ruleset: Ruleset): MapUnit { fun getMapUnit(ruleset: Ruleset): MapUnit {

View File

@ -34,7 +34,7 @@ class CityScreenTileTable(private val cityScreen: CityScreen): Table() {
val stats = selectedTile.getTileStats(city, city.civInfo) val stats = selectedTile.getTileStats(city, city.civInfo)
innerTable.pad(5f) innerTable.pad(5f)
innerTable.add( MarkupRenderer.render(selectedTile.toMarkup(city.civInfo)) { innerTable.add( MarkupRenderer.render(selectedTile.toMarkup(city.civInfo), noLinkImages = true) {
// Sorry, this will leave the city screen // Sorry, this will leave the city screen
UncivGame.Current.setScreen(CivilopediaScreen(city.civInfo.gameInfo.ruleSet, link = it)) UncivGame.Current.setScreen(CivilopediaScreen(city.civInfo.gameInfo.ruleSet, link = it))
} ) } )

View File

@ -58,7 +58,7 @@ class ConstructionInfoTable(val city: CityInfo): Table() {
val description: String = when (construction) { val description: String = when (construction) {
is BaseUnit -> construction.getDescription(true) is BaseUnit -> construction.getDescription()
is Building -> construction.getDescription(true, city, city.civInfo.gameInfo.ruleSet) is Building -> construction.getDescription(true, city, city.civInfo.gameInfo.ruleSet)
is PerpetualConstruction -> construction.description.replace("[rate]", "[${construction.getConversionRate(city)}]").tr() is PerpetualConstruction -> construction.description.replace("[rate]", "[${construction.getConversionRate(city)}]").tr()
else -> "" // Should never happen else -> "" // Should never happen

View File

@ -93,7 +93,7 @@ class CivilopediaScreen(
if (category !in categoryToButtons) return // defense against being passed a bad selector if (category !in categoryToButtons) return // defense against being passed a bad selector
categoryToButtons[category]!!.color = Color.BLUE categoryToButtons[category]!!.color = Color.BLUE
if (category !in categoryToEntries) return // defense, allowing buggy panes to remain emtpy while others work if (category !in categoryToEntries) return // defense, allowing buggy panes to remain empty while others work
var entries = categoryToEntries[category]!! var entries = categoryToEntries[category]!!
if (category != CivilopediaCategories.Difficulty) // this is the only case where we need them in order if (category != CivilopediaCategories.Difficulty) // this is the only case where we need them in order
entries = entries.sortedBy { it.name.tr() } // Alphabetical order of localized names entries = entries.sortedBy { it.name.tr() } // Alphabetical order of localized names
@ -215,7 +215,7 @@ class CivilopediaScreen(
.map { .map {
CivilopediaEntry( CivilopediaEntry(
it.name, it.name,
it.getDescription(false), "",
CivilopediaCategories.Unit.getImage?.invoke(it.name, imageSize), CivilopediaCategories.Unit.getImage?.invoke(it.name, imageSize),
(it as? ICivilopediaText).takeUnless { ct -> ct==null || ct.isCivilopediaTextEmpty() } (it as? ICivilopediaText).takeUnless { ct -> ct==null || ct.isCivilopediaTextEmpty() }
) )
@ -253,7 +253,7 @@ class CivilopediaScreen(
.map { .map {
CivilopediaEntry( CivilopediaEntry(
it.key.replace("_", " "), it.key.replace("_", " "),
it.value.joinToString("\n\n") { line -> line.tr() }, "",
// CivilopediaCategories.Tutorial.getImage?.invoke(it.name, imageSize) // CivilopediaCategories.Tutorial.getImage?.invoke(it.name, imageSize)
flavour = SimpleCivilopediaText( flavour = SimpleCivilopediaText(
sequenceOf(FormattedLine(extraImage = it.key)), sequenceOf(FormattedLine(extraImage = it.key)),
@ -317,6 +317,7 @@ class CivilopediaScreen(
entrySplitPane.splitAmount = 0.3f entrySplitPane.splitAmount = 0.3f
entryTable.addActor(entrySplitPane) entryTable.addActor(entrySplitPane)
entrySplitPane.setFillParent(true) entrySplitPane.setFillParent(true)
entrySplitPane.pack() // ensure selectEntry has correct entrySelectScroll.height and maxY
if (link.isEmpty() || '/' !in link) if (link.isEmpty() || '/' !in link)
selectCategory(category) selectCategory(category)

View File

@ -8,28 +8,59 @@ import com.badlogic.gdx.utils.Align
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.models.stats.INamed import com.unciv.models.stats.INamed
import com.unciv.ui.utils.* import com.unciv.ui.utils.*
import kotlin.math.max
/* Ideas:
* - Now we're using a Table container and inside one Table per line. Rendering order, in view of
* texture swaps, is per Group, as this goes by ZIndex and that is implemented as actual index
* into the parent's children array. So, we're SOL to get the number of texture switches down
* with this structure, as many lines will require at least 2 texture switches.
* We *could* instead try go for one big table with 4 columns (3 images, plus rest)
* and use colspan - then group all images separate from labels via ZIndex. To-Do later.
* - Do bold using Distance field fonts wrapped in something like [maltaisn/msdf-gdx](https://github.com/maltaisn/msdf-gdx)
* - Do strikethrough by stacking a line on top (as rectangle with background like the separator but thinner)
*/
/** Represents a text line with optional linking capability. // Kdoc not using the @property syntax because Android Studio 4.2.2 renders those _twice_
/** Represents a decorated text line with optional linking capability.
* A line can have [text] with optional [size], [color], [indent] or as [header];
* and up to three icons: [link], [object][icon], [star][starred] in that order.
* Special cases: * Special cases:
* - Standalone [image][extraImage] from atlas or from ExtraImages
* - A separator line ([separator])
* - Automatic external links (no [text] but [link] begins with a URL protocol) * - Automatic external links (no [text] but [link] begins with a URL protocol)
*
* @param text Text to display.
* @param link Create link: Line gets a 'Link' icon and is linked to either
* an Unciv object (format `category/entryname`) or an external URL.
* @param extraImage Display an Image instead of text. Can be a path as understood by
* [ImageGetter.getImage] or the name of a png or jpg in ExtraImages.
* @param imageSize Width of the [extraImage], height is calculated preserving aspect ratio. Defaults to available width.
* @param header Header level. 1 means double text size and decreases from there.
* @param separator Renders a separator line instead of text.
*/ */
class FormattedLine ( class FormattedLine (
/** Text to display. */
val text: String = "", val text: String = "",
/** Create link: Line gets a 'Link' icon and is linked to either
* an Unciv object (format `category/entryname`) or an external URL. */
val link: String = "", val link: String = "",
/** Display an Unciv object's icon inline but do not link (format `category/entryname`). */
val icon: String = "",
/** Display an Image instead of text, [sized][imageSize]. Can be a path as understood by
* [ImageGetter.getImage] or the name of a png or jpg in ExtraImages. */
val extraImage: String = "", val extraImage: String = "",
/** Width of the [extraImage], height is calculated preserving aspect ratio. Defaults to available width. */
val imageSize: Float = Float.NaN, val imageSize: Float = Float.NaN,
/** Text size, defaults to 18. Use [size] or [header] but not both. */
val size: Int = Int.MIN_VALUE,
/** Header level. 1 means double text size and decreases from there. */
val header: Int = 0, val header: Int = 0,
/** Indentation: 0 = text will follow icons with a little padding,
* 1 = aligned to a little more than 3 icons, each step above that adds 30f. */
val indent: Int = 0,
/** Defines vertical padding between rows, defaults to 5f. */
val padding: Float = Float.NaN,
/** Sets text color, accepts Java names or 6/3-digit web colors (e.g. #FFA040). */
val color: String = "",
/** Renders a separator line instead of text. Can be combined only with [color] and [size] (line width, default 2) */
val separator: Boolean = false, val separator: Boolean = false,
/** Decorates text with a star icon - if set, it receives the [color] instead of the text. */
val starred: Boolean = false,
/** Centers the line (and turns off wrap) */
val centered: Boolean = false
) { ) {
// Note: This gets directly deserialized by Json - please keep all attributes meant to be read // Note: This gets directly deserialized by Json - please keep all attributes meant to be read
// from json in the primary constructor parameters above. Everything else should be a fun(), // from json in the primary constructor parameters above. Everything else should be a fun(),
@ -53,31 +84,89 @@ class FormattedLine (
} }
} }
/** Translates [centered] into [libGdx][Gdx] [Align] value */
val align: Int by lazy {if (centered) Align.center else Align.left}
private val iconToDisplay: String by lazy {
if (icon.isNotEmpty()) icon else if (linkType == LinkType.Internal) link else ""
}
private val textToDisplay: String by lazy { private val textToDisplay: String by lazy {
if (text.isEmpty() && linkType == LinkType.External) link else text if (text.isEmpty() && linkType == LinkType.External) link else text
} }
/** Returns true if this formatted line will not display anything */
fun isEmpty(): Boolean = text.isEmpty() && extraImage.isEmpty() && link.isEmpty() && !separator
/** Constants used by [FormattedLine] /** Retrieves the parsed [Color] corresponding to the [color] property (String)*/
* @property defaultSize Mirrors default text size as defined elsewhere val displayColor: Color by lazy { parseColor() }
* @property headerSizes Array of text sizes to translate the [header] attribute
/** Returns true if this formatted line will not display anything */
fun isEmpty(): Boolean = text.isEmpty() && extraImage.isEmpty() &&
!starred && icon.isEmpty() && link.isEmpty()
/** Self-check to potentially support the mod checker
* @return `null` if no problems found, or multiline String naming problems.
*/ */
@Suppress("unused")
fun unsupportedReason(): String? {
val reasons = sequence {
if (text.isNotEmpty() && separator) yield("separator and text are incompatible")
if (extraImage.isNotEmpty() && link.isNotEmpty()) yield("extraImage and other options except imageSize are incompatible")
if (header != 0 && size != Int.MIN_VALUE) yield("use either size or header but not both")
// ...
}
return reasons.joinToString { "\n" }.takeIf { it.isNotEmpty() }
}
/** Constants used by [FormattedLine] */
companion object { companion object {
/** Mirrors default [text] size as used by [toLabel] */
const val defaultSize = 18 const val defaultSize = 18
/** Array of text sizes to translate the [header] attribute */
val headerSizes = arrayOf(defaultSize,36,32,27,24,21,15,12,9) // pretty arbitrary, yes val headerSizes = arrayOf(defaultSize,36,32,27,24,21,15,12,9) // pretty arbitrary, yes
/** Default color for [text] _and_ icons */
val defaultColor: Color = Color.WHITE val defaultColor: Color = Color.WHITE
/** Internal path to the [Link][link] image */
const val linkImage = "OtherIcons/Link"
/** Internal path to the [Star][starred] image */
const val starImage = "OtherIcons/Star"
/** Default inline icon size */
const val minIconSize = 30f
/** Padding added to the right of each icon */
const val iconPad = 5f
/** Padding distance per [indent] level */
const val indentPad = 30f
} }
/** Extension: determines if a [String] looks like a link understood by the OS */ /** Extension: determines if a [String] looks like a link understood by the OS */
private fun String.hasProtocol() = startsWith("http://") || startsWith("https://") || startsWith("mailto:") private fun String.hasProtocol() = startsWith("http://") || startsWith("https://") || startsWith("mailto:")
/** Extension: determines if a section of a [String] is composed entirely of hex digits
* @param start starting index
* @param length length of section (if == 0 [isHex] returns `true`, if receiver too short [isHex] returns `false`)
*/
private fun String.isHex(start: Int, length: Int) =
when {
length == 0 -> false
start + length > this.length -> false
substring(start, start + length).all { it.isDigit() || it in 'a'..'f' || it in 'A'..'F' } -> true
else -> false
}
/** Parse a json-supplied color string to [Color], defaults to [defaultColor]. */
private fun parseColor(): Color {
if (color.isEmpty()) return defaultColor
if (color[0] == '#' && color.isHex(1,3)) {
if (color.isHex(1,6)) return Color.valueOf(color)
val hex6 = String(charArrayOf(color[1], color[1], color[2], color[2], color[3], color[3]))
return Color.valueOf(hex6)
}
return defaultColor
}
/** /**
* Renders the formatted line as a scene2d [Actor] (currently always a [Table]) * Renders the formatted line as a scene2d [Actor] (currently always a [Table])
* @param labelWidth Total width to render into, needed to support wrap on Labels. * @param labelWidth Total width to render into, needed to support wrap on Labels.
* @param noLinkImages Omit visual indicator that a line is linked.
*/ */
fun render(labelWidth: Float): Actor { fun render(labelWidth: Float, noLinkImages: Boolean = false): Actor {
if (extraImage.isNotEmpty()) { if (extraImage.isNotEmpty()) {
val table = Table(CameraStageBaseScreen.skin) val table = Table(CameraStageBaseScreen.skin)
try { try {
@ -101,21 +190,70 @@ class FormattedLine (
val fontSize = when { val fontSize = when {
header in headerSizes.indices -> headerSizes[header] header in headerSizes.indices -> headerSizes[header]
else -> defaultSize size == Int.MIN_VALUE -> defaultSize
else -> size
} }
val labelColor = if(starred) defaultColor else displayColor
val table = Table(CameraStageBaseScreen.skin) val table = Table(CameraStageBaseScreen.skin)
var iconCount = 0
val iconSize = max(minIconSize, fontSize * 1.5f)
if (linkType != LinkType.None && !noLinkImages) {
table.add( ImageGetter.getImage(linkImage) ).size(iconSize).padRight(iconPad)
iconCount++
}
if (!noLinkImages)
iconCount += renderIcon(table, iconToDisplay, iconSize)
if (starred) {
val image = ImageGetter.getImage(starImage)
image.color = displayColor
table.add(image).size(iconSize).padRight(iconPad)
iconCount++
}
if (textToDisplay.isNotEmpty()) { if (textToDisplay.isNotEmpty()) {
val label = if (fontSize == defaultSize) textToDisplay.toLabel() val usedWidth = iconCount * (iconSize + iconPad)
else textToDisplay.toLabel(defaultColor,fontSize) val padIndent = when {
label.wrap = labelWidth > 0f centered -> -usedWidth
indent == 0 && iconCount == 0 -> 0f
indent == 0 -> iconPad
else -> (indent-1) * indentPad + 3 * minIconSize + 4 * iconPad - usedWidth
}
val label = if (fontSize == defaultSize && labelColor == defaultColor) textToDisplay.toLabel()
else textToDisplay.toLabel(labelColor,fontSize)
label.wrap = !centered && labelWidth > 0f
label.setAlignment(align)
if (labelWidth == 0f) if (labelWidth == 0f)
table.add(label) table.add(label)
.padLeft(padIndent).align(align)
else else
table.add(label).width(labelWidth) table.add(label).width(labelWidth - usedWidth - padIndent)
.padLeft(padIndent).align(align)
} }
return table return table
} }
/** Place a RuleSet object icon.
* @return 1 if successful for easy counting
*/
private fun renderIcon(table: Table, iconToDisplay: String, iconSize: Float): Int {
// prerequisites: iconToDisplay has form "category/name", category can be mapped to
// a `CivilopediaCategories`, and that knows how to get an Image.
if (iconToDisplay.isEmpty()) return 0
val parts = iconToDisplay.split('/', limit = 2)
if (parts.size != 2) return 0
val category = CivilopediaCategories.fromLink(parts[0]) ?: return 0
if (category.getImage == null) return 0
// That Enum custom property is a nullable reference to a lambda which
// in turn is allowed to return null. Sorry, but without `!!` the code
// won't compile and with we would get the incorrect warning.
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
val image = category.getImage!!(parts[1], iconSize) ?: return 0
table.add(image).size(iconSize).padRight(iconPad)
return 1
}
// Debug visualization only // Debug visualization only
override fun toString(): String { override fun toString(): String {
return when { return when {
@ -131,22 +269,28 @@ class FormattedLine (
/** Makes [renderer][render] available outside [ICivilopediaText] */ /** Makes [renderer][render] available outside [ICivilopediaText] */
object MarkupRenderer { object MarkupRenderer {
/** Height of empty line (`FormattedLine()`) - about half a normal text line, independent of font size */
private const val emptyLineHeight = 10f private const val emptyLineHeight = 10f
/** Default cell padding of non-empty lines */
private const val defaultPadding = 2.5f private const val defaultPadding = 2.5f
/** Padding above a [separator][FormattedLine.separator] line */
private const val separatorTopPadding = 5f private const val separatorTopPadding = 5f
/** Padding below a [separator][FormattedLine.separator] line */
private const val separatorBottomPadding = 15f private const val separatorBottomPadding = 15f
/** /**
* Build a Gdx [Table] showing [formatted][FormattedLine] [content][lines]. * Build a Gdx [Table] showing [formatted][FormattedLine] [content][lines].
* *
* @param lines The formatted content to render.
* @param labelWidth Available width needed for wrapping labels and [centered][FormattedLine.centered] attribute. * @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 noLinkImages Flag to omit link images (but not linking itself)
* @param linkAction Delegate to call for internal links. Leave null to suppress linking. * @param linkAction Delegate to call for internal links. Leave null to suppress linking.
*/ */
fun render( fun render(
lines: Collection<FormattedLine>, lines: Collection<FormattedLine>,
labelWidth: Float = 0f, labelWidth: Float = 0f,
padding: Float = defaultPadding, padding: Float = defaultPadding,
noLinkImages: Boolean = false,
linkAction: ((id: String) -> Unit)? = null linkAction: ((id: String) -> Unit)? = null
): Table { ): Table {
val skin = CameraStageBaseScreen.skin val skin = CameraStageBaseScreen.skin
@ -157,10 +301,11 @@ object MarkupRenderer {
continue continue
} }
if (line.separator) { if (line.separator) {
table.addSeparator().pad(separatorTopPadding, 0f, separatorBottomPadding, 0f) table.addSeparator(line.displayColor, 1, if (line.size == Int.MIN_VALUE) 2f else line.size.toFloat())
.pad(separatorTopPadding, 0f, separatorBottomPadding, 0f)
continue continue
} }
val actor = line.render(labelWidth) val actor = line.render(labelWidth, noLinkImages)
if (line.linkType == FormattedLine.LinkType.Internal && linkAction != null) if (line.linkType == FormattedLine.LinkType.Internal && linkAction != null)
actor.onClick { actor.onClick {
linkAction(line.link) linkAction(line.link)
@ -170,9 +315,9 @@ object MarkupRenderer {
Gdx.net.openURI(line.link) Gdx.net.openURI(line.link)
} }
if (labelWidth == 0f) if (labelWidth == 0f)
table.add(actor).row() table.add(actor).align(line.align).row()
else else
table.add(actor).width(labelWidth).row() table.add(actor).width(labelWidth).align(line.align).row()
} }
return table.apply { pack() } return table.apply { pack() }
} }

View File

@ -22,7 +22,7 @@ class TileInfoTable(private val viewingCiv :CivilizationInfo) : Table(CameraStag
if (tile != null && (UncivGame.Current.viewEntireMapForDebug || viewingCiv.exploredTiles.contains(tile.position)) ) { if (tile != null && (UncivGame.Current.viewEntireMapForDebug || viewingCiv.exploredTiles.contains(tile.position)) ) {
add(getStatsTable(tile)) add(getStatsTable(tile))
add( MarkupRenderer.render(tile.toMarkup(viewingCiv), padding = 0f) { add( MarkupRenderer.render(tile.toMarkup(viewingCiv), padding = 0f, noLinkImages = true) {
UncivGame.Current.setScreen(CivilopediaScreen(viewingCiv.gameInfo.ruleSet, link = it)) UncivGame.Current.setScreen(CivilopediaScreen(viewingCiv.gameInfo.ruleSet, link = it))
} ).pad(5f) } ).pad(5f)
// For debug only! // For debug only!