mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-04 15:27:50 +07:00
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:
BIN
android/Images/OtherIcons/Link.png
Normal file
BIN
android/Images/OtherIcons/Link.png
Normal file
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 |
@ -37,7 +37,10 @@
|
||||
"cost": 40,
|
||||
"obsoleteTech": "Metal Casting",
|
||||
"upgradesTo": "Swordsman",
|
||||
"attackSound": "nonmetalhit"
|
||||
"attackSound": "nonmetalhit",
|
||||
"civilopediaText": [
|
||||
{"text": "This is your basic, club-swinging fighter."}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Maori Warrior",
|
||||
|
@ -174,7 +174,7 @@ class Nation : INamed {
|
||||
textList += unit.name.tr() + " - " + "Replaces [${unit.replaces}], which is not found in the ruleset!".tr()
|
||||
} else {
|
||||
textList += unit.name.tr()
|
||||
textList += " " + unit.getDescription(true).split("\n").joinToString("\n ")
|
||||
textList += " " + unit.getDescription().split("\n").joinToString("\n ")
|
||||
}
|
||||
|
||||
textList += ""
|
||||
|
@ -7,8 +7,10 @@ import com.unciv.logic.civilization.CivilizationInfo
|
||||
import com.unciv.logic.map.MapUnit
|
||||
import com.unciv.models.ruleset.Ruleset
|
||||
import com.unciv.models.ruleset.Unique
|
||||
import com.unciv.models.stats.INamed
|
||||
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 kotlin.math.pow
|
||||
|
||||
@ -16,7 +18,7 @@ import kotlin.math.pow
|
||||
|
||||
/** 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 */
|
||||
class BaseUnit : INamed, IConstruction {
|
||||
class BaseUnit : INamed, IConstruction, CivilopediaText() {
|
||||
|
||||
override lateinit var name: String
|
||||
var cost: Int = 0
|
||||
@ -52,36 +54,115 @@ class BaseUnit : INamed, IConstruction {
|
||||
return infoList.joinToString()
|
||||
}
|
||||
|
||||
fun getDescription(forPickerScreen: Boolean): String {
|
||||
val sb = StringBuilder()
|
||||
/** Generate description as multi-line string for Nation description addUniqueUnitsText and CityScreen addSelectedConstructionTable */
|
||||
fun getDescription(): String {
|
||||
val lines = mutableListOf<String>()
|
||||
for ((resource, amount) in getResourceRequirements()) {
|
||||
if (amount == 1) sb.appendLine("Consumes 1 [$resource]".tr())
|
||||
else sb.appendLine("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())
|
||||
lines += if (amount == 1) "Consumes 1 [$resource]".tr()
|
||||
else "Consumes [$amount] [$resource]".tr()
|
||||
}
|
||||
var strengthLine = ""
|
||||
if (strength != 0) {
|
||||
sb.append("$strength${Fonts.strength}, ")
|
||||
if (rangedStrength != 0) sb.append("$rangedStrength${Fonts.rangedStrength}, ")
|
||||
if (rangedStrength != 0) sb.append("$range${Fonts.range}, ")
|
||||
strengthLine += "$strength${Fonts.strength}, "
|
||||
if (rangedStrength != 0)
|
||||
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)
|
||||
sb.appendLine(unique.tr())
|
||||
lines += unique.tr()
|
||||
|
||||
if (promotions.isNotEmpty()) {
|
||||
sb.append((if (promotions.size == 1) "Free promotion:" else "Free promotions:").tr())
|
||||
sb.appendLine(promotions.joinToString(", ", " ") { it.tr() })
|
||||
val prefix = "Free promotion${if (promotions.size == 1) "" else "s"}:".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 {
|
||||
|
@ -34,7 +34,7 @@ class CityScreenTileTable(private val cityScreen: CityScreen): Table() {
|
||||
val stats = selectedTile.getTileStats(city, city.civInfo)
|
||||
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
|
||||
UncivGame.Current.setScreen(CivilopediaScreen(city.civInfo.gameInfo.ruleSet, link = it))
|
||||
} )
|
||||
|
@ -58,7 +58,7 @@ class ConstructionInfoTable(val city: CityInfo): Table() {
|
||||
|
||||
|
||||
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 PerpetualConstruction -> construction.description.replace("[rate]", "[${construction.getConversionRate(city)}]").tr()
|
||||
else -> "" // Should never happen
|
||||
|
@ -93,7 +93,7 @@ class CivilopediaScreen(
|
||||
if (category !in categoryToButtons) return // defense against being passed a bad selector
|
||||
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]!!
|
||||
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
|
||||
@ -215,7 +215,7 @@ class CivilopediaScreen(
|
||||
.map {
|
||||
CivilopediaEntry(
|
||||
it.name,
|
||||
it.getDescription(false),
|
||||
"",
|
||||
CivilopediaCategories.Unit.getImage?.invoke(it.name, imageSize),
|
||||
(it as? ICivilopediaText).takeUnless { ct -> ct==null || ct.isCivilopediaTextEmpty() }
|
||||
)
|
||||
@ -253,7 +253,7 @@ class CivilopediaScreen(
|
||||
.map {
|
||||
CivilopediaEntry(
|
||||
it.key.replace("_", " "),
|
||||
it.value.joinToString("\n\n") { line -> line.tr() },
|
||||
"",
|
||||
// CivilopediaCategories.Tutorial.getImage?.invoke(it.name, imageSize)
|
||||
flavour = SimpleCivilopediaText(
|
||||
sequenceOf(FormattedLine(extraImage = it.key)),
|
||||
@ -317,6 +317,7 @@ class CivilopediaScreen(
|
||||
entrySplitPane.splitAmount = 0.3f
|
||||
entryTable.addActor(entrySplitPane)
|
||||
entrySplitPane.setFillParent(true)
|
||||
entrySplitPane.pack() // ensure selectEntry has correct entrySelectScroll.height and maxY
|
||||
|
||||
if (link.isEmpty() || '/' !in link)
|
||||
selectCategory(category)
|
||||
|
@ -8,28 +8,59 @@ import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.models.ruleset.Ruleset
|
||||
import com.unciv.models.stats.INamed
|
||||
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:
|
||||
* - Standalone [image][extraImage] from atlas or from ExtraImages
|
||||
* - A separator line ([separator])
|
||||
* - 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 (
|
||||
/** Text to display. */
|
||||
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 = "",
|
||||
/** 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 = "",
|
||||
/** Width of the [extraImage], height is calculated preserving aspect ratio. Defaults to available width. */
|
||||
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,
|
||||
/** 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,
|
||||
/** 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
|
||||
// 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 {
|
||||
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]
|
||||
* @property defaultSize Mirrors default text size as defined elsewhere
|
||||
* @property headerSizes Array of text sizes to translate the [header] attribute
|
||||
/** Retrieves the parsed [Color] corresponding to the [color] property (String)*/
|
||||
val displayColor: Color by lazy { parseColor() }
|
||||
|
||||
/** 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 {
|
||||
/** Mirrors default [text] size as used by [toLabel] */
|
||||
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
|
||||
/** Default color for [text] _and_ icons */
|
||||
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 */
|
||||
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])
|
||||
* @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()) {
|
||||
val table = Table(CameraStageBaseScreen.skin)
|
||||
try {
|
||||
@ -101,21 +190,70 @@ class FormattedLine (
|
||||
|
||||
val fontSize = when {
|
||||
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)
|
||||
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()) {
|
||||
val label = if (fontSize == defaultSize) textToDisplay.toLabel()
|
||||
else textToDisplay.toLabel(defaultColor,fontSize)
|
||||
label.wrap = labelWidth > 0f
|
||||
val usedWidth = iconCount * (iconSize + iconPad)
|
||||
val padIndent = when {
|
||||
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)
|
||||
table.add(label)
|
||||
.padLeft(padIndent).align(align)
|
||||
else
|
||||
table.add(label).width(labelWidth)
|
||||
table.add(label).width(labelWidth - usedWidth - padIndent)
|
||||
.padLeft(padIndent).align(align)
|
||||
}
|
||||
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
|
||||
override fun toString(): String {
|
||||
return when {
|
||||
@ -131,22 +269,28 @@ 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 = 5f
|
||||
/** Padding below a [separator][FormattedLine.separator] line */
|
||||
private const val separatorBottomPadding = 15f
|
||||
|
||||
/**
|
||||
* 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 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.
|
||||
*/
|
||||
fun render(
|
||||
lines: Collection<FormattedLine>,
|
||||
labelWidth: Float = 0f,
|
||||
padding: Float = defaultPadding,
|
||||
noLinkImages: Boolean = false,
|
||||
linkAction: ((id: String) -> Unit)? = null
|
||||
): Table {
|
||||
val skin = CameraStageBaseScreen.skin
|
||||
@ -157,10 +301,11 @@ object MarkupRenderer {
|
||||
continue
|
||||
}
|
||||
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
|
||||
}
|
||||
val actor = line.render(labelWidth)
|
||||
val actor = line.render(labelWidth, noLinkImages)
|
||||
if (line.linkType == FormattedLine.LinkType.Internal && linkAction != null)
|
||||
actor.onClick {
|
||||
linkAction(line.link)
|
||||
@ -170,9 +315,9 @@ object MarkupRenderer {
|
||||
Gdx.net.openURI(line.link)
|
||||
}
|
||||
if (labelWidth == 0f)
|
||||
table.add(actor).row()
|
||||
table.add(actor).align(line.align).row()
|
||||
else
|
||||
table.add(actor).width(labelWidth).row()
|
||||
table.add(actor).width(labelWidth).align(line.align).row()
|
||||
}
|
||||
return table.apply { pack() }
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ class TileInfoTable(private val viewingCiv :CivilizationInfo) : Table(CameraStag
|
||||
|
||||
if (tile != null && (UncivGame.Current.viewEntireMapForDebug || viewingCiv.exploredTiles.contains(tile.position)) ) {
|
||||
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))
|
||||
} ).pad(5f)
|
||||
// For debug only!
|
||||
|
Reference in New Issue
Block a user