diff --git a/android/ImagesToNotAddToGame/WorldScreenHelp.xcf b/android/ImagesToNotAddToGame/WorldScreenHelp.xcf new file mode 100644 index 0000000000..a8ec956faf Binary files /dev/null and b/android/ImagesToNotAddToGame/WorldScreenHelp.xcf differ diff --git a/android/assets/ExtraImages/World_Screen.png b/android/assets/ExtraImages/World_Screen.png new file mode 100644 index 0000000000..b3571e9b13 Binary files /dev/null and b/android/assets/ExtraImages/World_Screen.png differ diff --git a/android/assets/jsons/Civ V - Vanilla/TileImprovements.json b/android/assets/jsons/Civ V - Vanilla/TileImprovements.json index 66123000b1..29f95126ce 100644 --- a/android/assets/jsons/Civ V - Vanilla/TileImprovements.json +++ b/android/assets/jsons/Civ V - Vanilla/TileImprovements.json @@ -100,14 +100,20 @@ "turnsToBuild": 4, "techRequired": "The Wheel", "uniques": ["Can be built outside your borders", "Costs [1] gold per turn when in your territory"], - "shortcutKey": "R" + "shortcutKey": "R", + "civilopediaText": [ + {text:"Reduces movement cost to ½ if the other tile also has a Road or Railroad"}, + {text:"Reduces movement cost to ⅓ with Machinery",link:"Technology/Machinery"}, + {text:"Requires Engineering to bridge rivers",link:"Technology/Engineering"} + ] }, { "name": "Railroad", "turnsToBuild": 4, "techRequired": "Railroad", "uniques": ["Can be built outside your borders", "Costs [2] gold per turn when in your territory"], - "shortcutKey": "R" + "shortcutKey": "R", + "civilopediaText": [{text:"Reduces movement cost to ⅒ if the other tile also has a Railroad"}] }, // Removals @@ -117,7 +123,8 @@ "terrainsCanBeBuiltOn": ["Forest"], "techRequired": "Mining", "uniques": ["Can be built outside your borders"], - "shortcutKey": "X" + "shortcutKey": "X", + "civilopediaText": [{text:"Provides a one-time Production bonus depending on distance to the closest city once finished"}] }, { "name": "Remove Jungle", @@ -184,7 +191,8 @@ }, { "name": "Citadel", - "uniques": ["Gives a defensive bonus of [100]%", "Deal 30 damage to adjacent enemy units", "Great Improvement", "Can be built just outside your borders"] + "uniques": ["Gives a defensive bonus of [100]%", "Deal 30 damage to adjacent enemy units", "Great Improvement", "Can be built just outside your borders"], + "civilopediaText": [{text:"Constructing it will take over the tiles around it and assign them to your closest city"}] }, //Civilization unique improvements @@ -210,8 +218,12 @@ "shortcutKey": "F" }, - { "name": "Ancient ruins", "uniques": ["Unpillagable"] }, - { "name": "City ruins", "uniques": ["Unpillagable"] }, - { "name": "City center", "uniques": ["Unpillagable"] }, - { "name": "Barbarian encampment", "uniques": ["Unpillagable"] } + { "name": "Ancient ruins", "uniques": ["Unpillagable"], + "civilopediaText": [{text:"Ancient ruins provide a one-time random bonus when explored"}] }, + { "name": "City ruins", "uniques": ["Unpillagable"], + "civilopediaText": [{text:"A bleak reminder of the destruction wreaked by War"}] }, + { "name": "City center", "uniques": ["Unpillagable"], + "civilopediaText": [{text:"Marks the center of a city"},{text:"Appearance changes with the technological era of the owning civilization"}] }, + { "name": "Barbarian encampment", "uniques": ["Unpillagable"], + "civilopediaText": [{text:"Home to uncivilized barbarians, will spawn a hostile unit from time to time"}] } ] diff --git a/android/assets/jsons/Tutorials.json b/android/assets/jsons/Tutorials.json index 6d0507fccb..00c4662ca1 100644 --- a/android/assets/jsons/Tutorials.json +++ b/android/assets/jsons/Tutorials.json @@ -132,5 +132,41 @@ "On the world screen the hotkeys are as follows:", "Space or 'N' - Next unit or turn\n'E' - Empire overview (last viewed page)\n'+', '-' - Zoom in / out\nHome - center on capital", "F1 - Open Civilopedia\nF2 - Empire overview Trades\nF3 - Empire overview Units\nF4 - Empire overview Diplomacy\nF5 - Social policies\nF6 - Technologies\nF7 - Empire overview Cities\nF8 - Victory Progress\nF9 - Empire overview Stats\nF10 - Empire overview Resources\nF11 - Quicksave\nF12 - Quickload", "Ctrl-R - Toggle tile resource display\nCtrl-Y - Toggle tile yield display\nCtrl-O - Game options\nCtrl-S - Save game\nCtrl-L - Load game" + ], + "World_Screen": [ +"", +"This is where you spend most of your time playing Unciv. See the world, control your units, access other screens from here.", +"", +"①: The menu button - civilopedia, save, load, options...", +"②: The player/nation whose turn it is - click for diplomacy overview.", +"③: The Technology Button - shows the tech tree which allows viewing or researching technologies.", +"④: The Social Policies Button - shows enacted and selectable policies, and with enough culture points you can enact new ones.", +"⑤: The Diplomacy Button - shows the diplomacy manager where you can talk to other civilizations.", +"⑥: Unit Action Buttons - while a unit is selected its possible actions appear here.", +"⑦: The unit/city info pane - shows information about a selected unit or city.", +"⑧: The name (and unit icon) of the selected unit or city, with current health if wounded.", +"⑨: The arrow buttons allow jumping to the next/previous unit.", +"⑩: For a selected unit, its promotions appear here, and clicking leads to the promotions screen for that unit.", +"⑪: Remaining/per turn movement points, strength and experience / XP needed for promotion. For cities, you get its combat strength.", +"⑫: This button closes the selected unit/city info pane.", +"⑬: This pane appears when you order a uint to attack an enemy. On top are attacker and defender with their respective base strengths.", +"⑭: Below that are strength boni or mali and health bars projecting before / after the attack.", +"⑮: The Attack Button - let blood flow!", +"⑯: The minimap shows an overview over the world, with known cities, terrain and fog of war. Clicking will position the main map.", +"⑰: To the side of the minimap are display feature toggling buttons - tile yield, worked indicator, show/hide resources. These mirror setting on the options screen and are hidden if you deactivate the minimap.", +"⑱: Tile information for the selected hex - current or potential yield, terrain, effects, present units, city located there and such.", +"⑲: Notifications - what happened during the last 'next turn' phase. Some are clickable to show a relevant place on the map, some even show several when you click repeatedly.", +"⑳: The Next Turn Button - unless there are things to do, in which case the label changes to 'next unit', 'pick policy' and so on.", +"ⓐ: The overview button leads to the empire overview screen with various tabs (the last one viewed is remembered) holding vital information about the state of your civilization in world.", +"ⓑ: The culture icon shows accumulated culture and culture needed for the next policy - in this case, the exclamation mark tells us a next policy can be enacted. Clicking is another way to the policies manager.", +"ⓒ: Your known strategic resources are displayed here with the available (usage already deducted) number - click to go to the resources overview screen.", +"ⓓ: Happiness/unhappiness balance and either golden age with turns left or accumulated happines with amount needed for a golden age is shown next to the smiley. Clicking also leads to the resources overview screen as luxury resources are a way to improve happiness.", +"ⓔ: The science icon shows the number of science points produced per turn. Clicking leads to the technology tree.", +"ⓕ: Number of turns played with translation into calendar years. Click to see the victory overview.", +"ⓖ: The number of gold coins in your treasury and income. Clicks lead to the Stats overview screen.", +"ⓧ: In the center of all this - the world map! Here, the \"X\" marks a spot outside the map. Yes, unless the wrap option was used, Unciv worlds are flat. Don't worry, your ships won't fall off the edge.", +"ⓨ: By the way, here's how an empire border looks like - it's in the national colours of the nation owning the territory.", +"ⓩ: And this is the red targeting circle that led to the attack pane back under ⑬.", +"What you don't see: The phone/tablet's back button will pop the question whether you wish to leave Unciv and go back to Real Life. On desktop versions, you can use the ESC key.", ] } diff --git a/core/src/com/unciv/models/Tutorial.kt b/core/src/com/unciv/models/Tutorial.kt index bd6c51e156..7fb4c88b67 100644 --- a/core/src/com/unciv/models/Tutorial.kt +++ b/core/src/com/unciv/models/Tutorial.kt @@ -34,7 +34,8 @@ enum class Tutorial(val value: String, val isCivilopedia: Boolean = !value.start CityExpansion("City_Expansion"), GreatPeople("Great_People"), RemovingTerrainFeatures("Removing_Terrain_Features"), - Keyboard("Keyboard") + Keyboard("Keyboard"), + WorldScreen("World_Screen"), ; companion object { diff --git a/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt b/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt index 0708c86bc3..1553964bfa 100644 --- a/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt +++ b/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt @@ -5,10 +5,12 @@ import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Unique import com.unciv.models.stats.NamedStats import com.unciv.models.translations.tr +import com.unciv.ui.civilopedia.FormattedLine +import com.unciv.ui.civilopedia.ICivilopediaText import java.util.* import kotlin.math.roundToInt -class TileImprovement : NamedStats() { +class TileImprovement : NamedStats(), ICivilopediaText { var terrainsCanBeBuiltOn: Collection = ArrayList() var techRequired: String? = null @@ -18,6 +20,9 @@ class TileImprovement : NamedStats() { val shortcutKey: Char? = null val turnsToBuild: Int = 0 // This is the base cost. + override var civilopediaText = listOf() + + fun getTurnsToBuild(civInfo: CivilizationInfo): Int { var realTurnsToBuild = turnsToBuild.toFloat() * civInfo.gameInfo.gameParameters.gameSpeed.modifier for (unique in civInfo.getMatchingUniques("-[]% tile improvement construction time")) { diff --git a/core/src/com/unciv/ui/civilopedia/CivilopediaScreen.kt b/core/src/com/unciv/ui/civilopedia/CivilopediaScreen.kt index f185523982..f0afdc9d41 100644 --- a/core/src/com/unciv/ui/civilopedia/CivilopediaScreen.kt +++ b/core/src/com/unciv/ui/civilopedia/CivilopediaScreen.kt @@ -28,6 +28,7 @@ class CivilopediaScreen( * @param name From [Ruleset] object [INamed.name] * @param description Multiline text * @param image Icon for button + * @param flavour [CivilopediaText] * @param y Y coordinate for scrolling to * @param height Cell height */ @@ -35,10 +36,11 @@ class CivilopediaScreen( val name: String, val description: String, val image: Actor? = null, + val flavour: ICivilopediaText? = null, val y: Float = 0f, // coordinates of button cell used to scroll to entry val height: Float = 0f ) { - fun withCoordinates(y: Float, height: Float) = CivilopediaEntry(name, description, image, y, height) + fun withCoordinates(y: Float, height: Float) = CivilopediaEntry(name, description, image, flavour, y, height) } private val categoryToEntries = LinkedHashMap>() @@ -48,6 +50,7 @@ class CivilopediaScreen( private val entrySelectTable = Table().apply { defaults().pad(6f).left() } private val entrySelectScroll: ScrollPane private val descriptionLabel = "".toLabel() + private val flavourTable = Table() private var currentCategory: CivilopediaCategories = CivilopediaCategories.Tutorial private var currentEntry: String = "" @@ -84,6 +87,7 @@ class CivilopediaScreen( entrySelectTable.clear() entryIndex.clear() descriptionLabel.setText("") + flavourTable.clear() for (button in categoryToButtons.values) button.color = Color.WHITE if (category !in categoryToButtons) return // defense against being passed a bad selector @@ -134,7 +138,22 @@ class CivilopediaScreen( } private fun selectEntry(entry: CivilopediaEntry) { currentEntry = entry.name - descriptionLabel.setText(entry.description) + if(entry.flavour != null && entry.flavour.replacesCivilopediaDescription()) { + descriptionLabel.setText("") + descriptionLabel.isVisible = false + } else { + descriptionLabel.setText(entry.description) + descriptionLabel.isVisible = true + } + flavourTable.clear() + if (entry.flavour != null) { + flavourTable.isVisible = true + flavourTable.add( + entry.flavour.assembleCivilopediaText(ruleset) + .renderCivilopediaText(stage.width * 0.5f) { selectLink(it) }) + } else { + flavourTable.isVisible = false + } entrySelectTable.children.forEach { it.color = if (it.name == entry.name) Color.BLUE else Color.WHITE } @@ -150,7 +169,8 @@ class CivilopediaScreen( CivilopediaEntry( it.name, it.getDescription(false, null, ruleset), - CivilopediaCategories.Building.getImage?.invoke(it.name, imageSize) + CivilopediaCategories.Building.getImage?.invoke(it.name, imageSize), + (it as? ICivilopediaText).takeUnless { ct -> ct==null || ct.isCivilopediaTextEmpty() } ) } categoryToEntries[CivilopediaCategories.Wonder] = ruleset.buildings.values @@ -159,7 +179,8 @@ class CivilopediaScreen( CivilopediaEntry( it.name, it.getDescription(false, null, ruleset), - CivilopediaCategories.Wonder.getImage?.invoke(it.name, imageSize) + CivilopediaCategories.Wonder.getImage?.invoke(it.name, imageSize), + (it as? ICivilopediaText).takeUnless { ct -> ct==null || ct.isCivilopediaTextEmpty() } ) } categoryToEntries[CivilopediaCategories.Resource] = ruleset.tileResources.values @@ -167,7 +188,8 @@ class CivilopediaScreen( CivilopediaEntry( it.name, it.getDescription(ruleset), - CivilopediaCategories.Resource.getImage?.invoke(it.name, imageSize) + CivilopediaCategories.Resource.getImage?.invoke(it.name, imageSize), + (it as? ICivilopediaText).takeUnless { ct -> ct==null || ct.isCivilopediaTextEmpty() } ) } categoryToEntries[CivilopediaCategories.Terrain] = ruleset.terrains.values @@ -175,7 +197,8 @@ class CivilopediaScreen( CivilopediaEntry( it.name, it.getDescription(ruleset), - CivilopediaCategories.Terrain.getImage?.invoke(it.name, imageSize) + CivilopediaCategories.Terrain.getImage?.invoke(it.name, imageSize), + (it as? ICivilopediaText).takeUnless { ct -> ct==null || ct.isCivilopediaTextEmpty() } ) } categoryToEntries[CivilopediaCategories.Improvement] = ruleset.tileImprovements.values @@ -183,7 +206,8 @@ class CivilopediaScreen( CivilopediaEntry( it.name, it.getDescription(ruleset, false), - CivilopediaCategories.Improvement.getImage?.invoke(it.name, imageSize) + CivilopediaCategories.Improvement.getImage?.invoke(it.name, imageSize), + (it as? ICivilopediaText).takeUnless { ct -> ct==null || ct.isCivilopediaTextEmpty() } ) } categoryToEntries[CivilopediaCategories.Unit] = ruleset.units.values @@ -192,7 +216,8 @@ class CivilopediaScreen( CivilopediaEntry( 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() } ) } categoryToEntries[CivilopediaCategories.Nation] = ruleset.nations.values @@ -201,7 +226,8 @@ class CivilopediaScreen( CivilopediaEntry( it.name, it.getUniqueString(ruleset, false), - CivilopediaCategories.Nation.getImage?.invoke(it.name, imageSize) + CivilopediaCategories.Nation.getImage?.invoke(it.name, imageSize), + (it as? ICivilopediaText).takeUnless { ct -> ct==null || ct.isCivilopediaTextEmpty() } ) } categoryToEntries[CivilopediaCategories.Technology] = ruleset.technologies.values @@ -209,7 +235,8 @@ class CivilopediaScreen( CivilopediaEntry( it.name, it.getDescription(ruleset), - CivilopediaCategories.Technology.getImage?.invoke(it.name, imageSize) + CivilopediaCategories.Technology.getImage?.invoke(it.name, imageSize), + (it as? ICivilopediaText).takeUnless { ct -> ct==null || ct.isCivilopediaTextEmpty() } ) } categoryToEntries[CivilopediaCategories.Promotion] = ruleset.unitPromotions.values @@ -217,7 +244,8 @@ class CivilopediaScreen( CivilopediaEntry( it.name, it.getDescription(ruleset.unitPromotions.values, true, ruleset), - CivilopediaCategories.Promotion.getImage?.invoke(it.name, imageSize) + CivilopediaCategories.Promotion.getImage?.invoke(it.name, imageSize), + (it as? ICivilopediaText).takeUnless { ct -> ct==null || ct.isCivilopediaTextEmpty() } ) } @@ -227,6 +255,9 @@ class CivilopediaScreen( 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)), + it.value.asSequence(), true) ) } @@ -279,6 +310,7 @@ class CivilopediaScreen( entrySelectTable.top() entrySelectScroll.setOverscroll(false, false) val descriptionTable = Table() + descriptionTable.add(flavourTable).row() descriptionLabel.wrap = true // requires explicit cell width! descriptionTable.add(descriptionLabel).width(stage.width * 0.5f).padTop(10f).row() val entrySplitPane = SplitPane(entrySelectScroll, ScrollPane(descriptionTable), false, skin) diff --git a/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt b/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt index feb1877b56..570f258dff 100644 --- a/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt +++ b/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt @@ -1,9 +1,12 @@ package com.unciv.ui.civilopedia import com.badlogic.gdx.Gdx +import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.utils.Align +import com.unciv.models.ruleset.Ruleset +import com.unciv.models.stats.INamed import com.unciv.ui.utils.* @@ -14,10 +17,19 @@ import com.unciv.ui.utils.* * @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 ( val text: String = "", val link: String = "", + val extraImage: String = "", + val imageSize: Float = Float.NaN, + val header: Int = 0, + val separator: 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(), @@ -46,7 +58,17 @@ class FormattedLine ( } /** Returns true if this formatted line will not display anything */ - fun isEmpty(): Boolean = text.isEmpty() && link.isEmpty() + 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 + */ + companion object { + const val defaultSize = 18 + val headerSizes = arrayOf(defaultSize,36,32,27,24,21,15,12,9) // pretty arbitrary, yes + val defaultColor: Color = Color.WHITE + } /** Extension: determines if a [String] looks like a link understood by the OS */ private fun String.hasProtocol() = startsWith("http://") || startsWith("https://") || startsWith("mailto:") @@ -56,9 +78,35 @@ class FormattedLine ( * @param labelWidth Total width to render into, needed to support wrap on Labels. */ fun render(labelWidth: Float): Actor { + if (extraImage.isNotEmpty()) { + val table = Table(CameraStageBaseScreen.skin) + try { + val image = when { + ImageGetter.imageExists(extraImage) -> + ImageGetter.getImage(extraImage) + Gdx.files.internal("ExtraImages/$extraImage.png").exists() -> + ImageGetter.getExternalImage("$extraImage.png") + Gdx.files.internal("ExtraImages/$extraImage.jpg").exists() -> + ImageGetter.getExternalImage("$extraImage.jpg") + else -> return table + } + val width = if (imageSize.isNaN()) labelWidth else imageSize + val height = width * image.height / image.width + table.add(image).size(width, height) + } catch (exception: Exception) { + println ("${exception.message}: ${exception.cause?.message}") + } + return table + } + + val fontSize = when { + header in headerSizes.indices -> headerSizes[header] + else -> defaultSize + } val table = Table(CameraStageBaseScreen.skin) if (textToDisplay.isNotEmpty()) { - val label = textToDisplay.toLabel() + val label = if (fontSize == defaultSize) textToDisplay.toLabel() + else textToDisplay.toLabel(defaultColor,fontSize) label.wrap = labelWidth > 0f if (labelWidth == 0f) table.add(label) @@ -67,12 +115,26 @@ class FormattedLine ( } return table } + + // Debug visualization only + override fun toString(): String { + return when { + isEmpty() -> "(empty)" + separator -> "(separator)" + extraImage.isNotEmpty() -> "(extraImage='$extraImage')" + header > 0 -> "(header=$header)'$text'" + linkType == LinkType.None -> "'$text'" + else -> "'$text'->$link" + } + } } /** Makes [renderer][render] available outside [ICivilopediaText] */ object MarkupRenderer { private const val emptyLineHeight = 10f private const val defaultPadding = 2.5f + private const val separatorTopPadding = 5f + private const val separatorBottomPadding = 15f /** * Build a Gdx [Table] showing [formatted][FormattedLine] [content][lines]. @@ -94,6 +156,10 @@ object MarkupRenderer { table.add().padTop(emptyLineHeight).row() continue } + if (line.separator) { + table.addSeparator().pad(separatorTopPadding, 0f, separatorBottomPadding, 0f) + continue + } val actor = line.render(labelWidth) if (line.linkType == FormattedLine.LinkType.Internal && linkAction != null) actor.onClick { @@ -111,3 +177,106 @@ object MarkupRenderer { return table.apply { pack() } } } + +/** Storage class for interface [ICivilopediaText] for use as base class */ +open class CivilopediaText : ICivilopediaText { + override var civilopediaText = listOf() +} +/** Storage class for instantiation of the simplest form containing only the lines collection */ +class SimpleCivilopediaText(lines: List, val isComplete: Boolean = false) : CivilopediaText() { + init { + civilopediaText = lines + } + override fun hasCivilopediaTextLines() = true + override fun replacesCivilopediaDescription() = isComplete + constructor(strings: Sequence, isComplete: Boolean = false) : this( + strings.map { FormattedLine(it) }.toList(), isComplete) + constructor(first: Sequence, strings: Sequence, isComplete: Boolean = false) : this( + (first + strings.map { FormattedLine(it) }).toList(), isComplete) +} + +/** Addon common to most ruleset game objects managing civilopedia display + * + * ### Usage: + * 1. Let [Ruleset] object implement this (e.g. by inheriting class [CivilopediaText] or adding var [civilopediaText] itself) + * 2. Add `"civilopediaText": ["",…],` in the json for these objects + * 3. Optionally override [getCivilopediaTextHeader] to supply a header line + * 4. Optionally override [getCivilopediaTextLines] to supply automatic stuff like tech prerequisites, uniques, etc. + * 4. Optionally override [assembleCivilopediaText] to handle assembly of the final set of lines yourself. + */ +interface ICivilopediaText { + /** List of strings supporting simple [formatting rules][FormattedLine] that [CivilopediaScreen] can render. + * May later be merged with automatic lines generated by the deriving class + * through overridden [getCivilopediaTextHeader] and/or [getCivilopediaTextLines] methods. + * + */ + var civilopediaText: List + + /** Generate header line from object metadata. + * Default implementation will pull [INamed.name] and render it in 150% normal font size. + * @return A [FormattedLine] that will be inserted on top + */ + fun getCivilopediaTextHeader(): FormattedLine? = + if (this is INamed) FormattedLine(name, header = 2) + else null + + /** Generate automatic lines from object metadata. + * + * Default implementation is empty - no need to call super in overrides. + * + * @param ruleset The current ruleset for the Civilopedia viewer + * @return A list of [FormattedLine]s that will be inserted before + * the first line of [civilopediaText] having a [link][FormattedLine.link] + */ + fun getCivilopediaTextLines(ruleset: Ruleset): List = listOf() + + /** Override this and return true to tell the Civilopedia that the legacy description is no longer needed */ + fun replacesCivilopediaDescription() = false + /** Override this and return true to tell the Civilopedia that this is not empty even if nothing came from json */ + fun hasCivilopediaTextLines() = false + /** Indicates that neither json nor getCivilopediaTextLines have content */ + fun isCivilopediaTextEmpty() = civilopediaText.isEmpty() && !hasCivilopediaTextLines() + + /** 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): CivilopediaText { + 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 (hasCivilopediaTextLines()) { + if (outerNotEmpty) yield(FormattedLine()) + yieldAll(getCivilopediaTextLines(ruleset)) + yield(FormattedLine()) + } + } + outerNotEmpty = true + yield(next) + } + if (!middleDone) { + if (outerNotEmpty && hasCivilopediaTextLines()) yield(FormattedLine()) + yieldAll(getCivilopediaTextLines(ruleset)) + } + } + return SimpleCivilopediaText(newLines.toList(), isComplete = true) + } +}