Spruced up Civilopedia - phase 2 - external links (#3981)

* Spruced up Civilopedia - phase 2 - external links

* Spruced up Civilopedia - phase 2 - patch1

* Spruced up Civilopedia - phase 2 - patch2
This commit is contained in:
SomeTroglodyte 2021-05-28 16:41:03 +02:00 committed by GitHub
parent c45ecebb0c
commit 97a36c3772
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 223 additions and 44 deletions

View File

@ -5,10 +5,13 @@ import com.unciv.logic.civilization.AlertType
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.PopupAlert
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.UniqueMap
import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.stats.Stats
import com.unciv.models.translations.tr
import com.unciv.ui.civilopedia.CivilopediaCategories
import com.unciv.ui.civilopedia.FormattedLine
import com.unciv.ui.utils.Fonts
import com.unciv.ui.utils.withItem
import com.unciv.ui.utils.withoutItem
@ -141,6 +144,28 @@ class CityConstructions {
return result
}
fun getProductionMarkup(ruleset: Ruleset): FormattedLine {
val currentConstructionSnapshot = currentConstructionFromQueue
if (currentConstructionSnapshot.isEmpty()) return FormattedLine()
val category = when {
ruleset.buildings[currentConstructionSnapshot]
?.let{ it.isWonder || it.isNationalWonder } == true ->
CivilopediaCategories.Wonder.name
currentConstructionSnapshot in ruleset.buildings ->
CivilopediaCategories.Building.name
currentConstructionSnapshot in ruleset.units ->
CivilopediaCategories.Unit.name
else -> ""
}
var label = currentConstructionSnapshot
if (!PerpetualConstruction.perpetualConstructionsMap.containsKey(currentConstructionSnapshot)) {
val turnsLeft = turnsToConstruction(currentConstructionSnapshot)
label += " - $turnsLeft${Fonts.turn}"
}
return if (category.isEmpty()) FormattedLine(label)
else FormattedLine(label, link="$category/$currentConstructionSnapshot")
}
fun getCurrentConstruction(): IConstruction = getConstruction(currentConstructionFromQueue)
fun isBuilt(buildingName: String): Boolean = builtBuildings.contains(buildingName)

View File

@ -10,6 +10,7 @@ import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.tile.*
import com.unciv.models.stats.Stats
import com.unciv.models.translations.tr
import com.unciv.ui.civilopedia.FormattedLine
import com.unciv.ui.utils.Fonts
import kotlin.math.abs
import kotlin.math.min
@ -144,7 +145,7 @@ open class TileInfo {
else if (!ruleset.tileResources.containsKey(resource!!)) throw Exception("Resource $resource does not exist in this ruleset!")
else ruleset.tileResources[resource!!]!!
fun getNaturalWonder(): Terrain =
private fun getNaturalWonder(): Terrain =
if (naturalWonder == null) throw Exception("No natural wonder exists for this tile!")
else ruleset.terrains[naturalWonder!!]!!
@ -179,8 +180,7 @@ open class TileInfo {
fun getBaseTerrain(): Terrain = baseTerrainObject
fun getOwner(): CivilizationInfo? {
val containingCity = getCity()
if (containingCity == null) return null
val containingCity = getCity() ?: return null
return containingCity.civInfo
}
@ -204,8 +204,7 @@ open class TileInfo {
fun hasUnique(unique: String) = getAllTerrains().any { it.uniques.contains(unique) }
fun getWorkingCity(): CityInfo? {
val civInfo = getOwner()
if (civInfo == null) return null
val civInfo = getOwner() ?: return null
return civInfo.cities.firstOrNull { it.isWorked(this) }
}
@ -260,7 +259,7 @@ open class TileInfo {
stats.add(getTileResource()) // resource base
if (resource.building != null && city != null && city.cityConstructions.isBuilt(resource.building!!)) {
val resourceBuilding = tileMap.gameInfo.ruleSet.buildings[resource.building!!]
if (resourceBuilding != null && resourceBuilding.resourceBonusStats != null)
if (resourceBuilding?.resourceBonusStats != null)
stats.add(resourceBuilding.resourceBonusStats!!) // resource-specific building (eg forge, stable) bonus
}
}
@ -337,7 +336,7 @@ open class TileInfo {
improvement.hasUnique("Can be built outside your borders")
// citadel can be built only next to or within own borders
|| improvement.hasUnique("Can be built just outside your borders")
&& neighbors.any { it.getOwner() == civInfo } && !civInfo.cities.isEmpty()
&& neighbors.any { it.getOwner() == civInfo } && civInfo.cities.isNotEmpty()
) -> false
improvement.uniqueObjects.any {
it.placeholderText == "Obsolete with []" && civInfo.tech.isResearched(it.params[0])
@ -346,10 +345,10 @@ open class TileInfo {
}
}
/** Without regards to what civinfo it is, a lot of the checks are just for the improvement on the tile.
/** Without regards to what CivInfo it is, a lot of the checks are just for the improvement on the tile.
* Doubles as a check for the map editor.
*/
fun canImprovementBeBuiltHere(improvement: TileImprovement, resourceIsVisible: Boolean = resource != null): Boolean {
private fun canImprovementBeBuiltHere(improvement: TileImprovement, resourceIsVisible: Boolean = resource != null): Boolean {
val topTerrain = getLastTerrain()
return when {
@ -358,7 +357,7 @@ open class TileInfo {
"Cannot be built on bonus resource" in improvement.uniques && resource != null
&& getTileResource().resourceType == ResourceType.Bonus -> false
// Road improvements can change on tiles withh irremovable improvements - nothing else can, though.
// Road improvements can change on tiles with irremovable improvements - nothing else can, though.
improvement.name != RoadStatus.Railroad.name && improvement.name != RoadStatus.Railroad.name
&& improvement.name != "Remove Road" && improvement.name != "Remove Railroad"
&& getTileImprovement().let { it != null && it.hasUnique("Irremovable") } -> false
@ -371,7 +370,7 @@ open class TileInfo {
improvement.uniqueObjects.filter { it.placeholderText == "Must be next to []" }.any {
val filter = it.params[0]
if (filter == "River") return@any !isAdjacentToRiver()
else return@any !neighbors.any { it.matchesUniqueFilter(filter) }
else return@any !neighbors.any { neighbor -> neighbor.matchesUniqueFilter(filter) }
} -> false
improvement.name == "Road" && roadStatus == RoadStatus.None && !isWater -> true
improvement.name == "Railroad" && this.roadStatus != RoadStatus.Railroad && !isWater -> true
@ -448,8 +447,20 @@ open class TileInfo {
return min(distance, wrappedDistance).toInt()
}
override fun toString(): String { // for debugging, it helps to see what you're doing
return toString(null)
/** Shows important properties of this tile for debugging _only_, it helps to see what you're doing */
override fun toString(): String {
val lineList = arrayListOf("TileInfo @($position)")
if (isCityCenter()) lineList += getCity()!!.name
lineList += baseTerrain
for (terrainFeature in terrainFeatures) lineList += terrainFeature
if (resource != null ) lineList += resource!!
if (naturalWonder != null) lineList += naturalWonder!!
if (roadStatus !== RoadStatus.None && !isCityCenter()) lineList += roadStatus.name
if (improvement != null) lineList += improvement!!
if (civilianUnit != null) lineList += civilianUnit!!.name + " - " + civilianUnit!!.civInfo.civName
if (militaryUnit != null) lineList += militaryUnit!!.name + " - " + militaryUnit!!.civInfo.civName
if (isImpassible()) lineList += Constants.impassable
return lineList.joinToString()
}
/** The two tiles have a river between them */
@ -481,8 +492,8 @@ open class TileInfo {
return true
}
fun toString(viewingCiv: CivilizationInfo?): String {
val lineList = ArrayList<String>() // more readable than StringBuilder, with same performance for our use-case
fun toMarkup(viewingCiv: CivilizationInfo?): ArrayList<FormattedLine> {
val lineList = ArrayList<FormattedLine>() // more readable than StringBuilder, with same performance for our use-case
val isViewableToPlayer = viewingCiv == null || UncivGame.Current.viewEntireMapForDebug
|| viewingCiv.viewableTiles.contains(this)
@ -490,42 +501,46 @@ open class TileInfo {
val city = getCity()!!
var cityString = city.name.tr()
if (isViewableToPlayer) cityString += " (" + city.health + ")"
lineList += cityString
lineList += FormattedLine(cityString)
if (UncivGame.Current.viewEntireMapForDebug || city.civInfo == viewingCiv)
lineList += city.cityConstructions.getProductionForTileInfo()
lineList += city.cityConstructions.getProductionMarkup(ruleset)
}
lineList += baseTerrain.tr()
for (terrainFeature in terrainFeatures) lineList += terrainFeature.tr()
if (resource != null && (viewingCiv == null || hasViewableResource(viewingCiv))) lineList += resource!!.tr()
if (naturalWonder != null) lineList += naturalWonder!!.tr()
if (roadStatus !== RoadStatus.None && !isCityCenter()) lineList += roadStatus.name.tr()
if (improvement != null) lineList += improvement!!.tr()
lineList += FormattedLine(baseTerrain, link="Terrain/$baseTerrain")
for (terrainFeature in terrainFeatures)
lineList += FormattedLine(terrainFeature, link="Terrain/$terrainFeature")
if (resource != null && (viewingCiv == null || hasViewableResource(viewingCiv)))
lineList += FormattedLine(resource!!, link="Resource/$resource")
if (naturalWonder != null)
lineList += FormattedLine(naturalWonder!!, link="Terrain/$naturalWonder")
if (roadStatus !== RoadStatus.None && !isCityCenter())
lineList += FormattedLine(roadStatus.name, link="Improvement/${roadStatus.name}")
if (improvement != null)
lineList += FormattedLine(improvement!!, link="Improvement/$improvement")
if (improvementInProgress != null && isViewableToPlayer) {
var line = "{$improvementInProgress}"
if (turnsToImprovement > 0) line += " - $turnsToImprovement${Fonts.turn}"
else line += " ({Under construction})"
lineList += line.tr()
val line = "{$improvementInProgress}" +
if (turnsToImprovement > 0) " - $turnsToImprovement${Fonts.turn}" else " ({Under construction})"
lineList += FormattedLine(line, link="Improvement/$improvementInProgress")
}
if (civilianUnit != null && isViewableToPlayer)
lineList += civilianUnit!!.name.tr() + " - " + civilianUnit!!.civInfo.civName.tr()
lineList += FormattedLine(civilianUnit!!.name.tr() + " - " + civilianUnit!!.civInfo.civName.tr(),
link="Unit/${civilianUnit!!.name}")
if (militaryUnit != null && isViewableToPlayer) {
var milUnitString = militaryUnit!!.name.tr()
if (militaryUnit!!.health < 100) milUnitString += "(" + militaryUnit!!.health + ")"
milUnitString += " - " + militaryUnit!!.civInfo.civName.tr()
lineList += milUnitString
val milUnitString = militaryUnit!!.name.tr() +
(if (militaryUnit!!.health < 100) "(" + militaryUnit!!.health + ")" else "") +
" - " + militaryUnit!!.civInfo.civName.tr()
lineList += FormattedLine(milUnitString, link="Unit/${militaryUnit!!.name}")
}
val defenceBonus = getDefensiveBonus()
if (defenceBonus != 0f) {
var defencePercentString = (defenceBonus * 100).toInt().toString() + "%"
if (!defencePercentString.startsWith("-")) defencePercentString = "+$defencePercentString"
lineList += "[$defencePercentString] to unit defence".tr()
lineList += FormattedLine("[$defencePercentString] to unit defence")
}
if (isImpassible()) lineList += Constants.impassable.tr()
if (isImpassible()) lineList += FormattedLine(Constants.impassable)
return lineList.joinToString("\n")
return lineList
}
fun hasEnemyInvisibleUnit(viewingCiv: CivilizationInfo): Boolean {
val unitsInTile = getUnits()
if (unitsInTile.none()) return false
@ -649,7 +664,7 @@ open class TileInfo {
private fun normalizeTileImprovement(ruleset: Ruleset) {
if (improvement!!.startsWith("StartingLocation") == true) {
if (improvement!!.startsWith("StartingLocation")) {
if (!isLand || getLastTerrain().impassable) improvement = null
return
}

View File

@ -7,6 +7,8 @@ import com.unciv.logic.map.TileInfo
import com.unciv.models.UncivSound
import com.unciv.models.stats.Stats
import com.unciv.models.translations.tr
import com.unciv.ui.civilopedia.CivilopediaScreen
import com.unciv.ui.civilopedia.MarkupRenderer
import com.unciv.ui.utils.*
import kotlin.math.roundToInt
@ -32,7 +34,10 @@ class CityScreenTileTable(private val cityScreen: CityScreen): Table() {
val stats = selectedTile.getTileStats(city, city.civInfo)
innerTable.pad(5f)
innerTable.add(selectedTile.toString(city.civInfo).toLabel()).colspan(2)
innerTable.add( MarkupRenderer.render(selectedTile.toMarkup(city.civInfo)) {
// Sorry, this will leave the city screen
UncivGame.Current.setScreen(CivilopediaScreen(city.civInfo.gameInfo.ruleSet, link = it))
} ).colspan(2)
innerTable.row()
innerTable.add(getTileStatsTable(stats)).row()

View File

@ -28,7 +28,11 @@ object CivilopediaImageGetters {
}
TerrainType.TerrainFeature -> {
tileInfo.terrainFeatures.add(terrain.name)
tileInfo.baseTerrain = terrain.occursOn.lastOrNull() ?: Constants.grassland
tileInfo.baseTerrain =
if (terrain.occursOn.isEmpty() || terrain.occursOn.contains(Constants.grassland))
Constants.grassland
else
terrain.occursOn.lastOrNull()!!
}
else ->
tileInfo.baseTerrain = terrain.name

View File

@ -0,0 +1,112 @@
package com.unciv.ui.civilopedia
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.unciv.ui.utils.*
/** Represents a text line with optional linking capability.
* Special cases:
* - 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.
*/
class FormattedLine (
val text: String = "",
val link: String = "",
) {
// 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(),
// have no backing field, be `by lazy` or use @Transient, Thank you.
/** Link types that can be used for [FormattedLine.link] */
enum class LinkType {
None,
/** Link points to a Civilopedia entry in the form `category/item` **/
Internal,
/** Link opens as URL in external App - begins with `https://`, `http://` or `mailto:` **/
External
}
/** The type of the [link]'s destination */
val linkType: LinkType by lazy {
when {
link.hasProtocol() -> LinkType.External
link.isNotEmpty() -> LinkType.Internal
else -> LinkType.None
}
}
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() && link.isEmpty()
/** Extension: determines if a [String] looks like a link understood by the OS */
private fun String.hasProtocol() = startsWith("http://") || startsWith("https://") || startsWith("mailto:")
/**
* 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.
*/
fun render(labelWidth: Float): Actor {
val table = Table(CameraStageBaseScreen.skin)
if (textToDisplay.isNotEmpty()) {
val label = textToDisplay.toLabel()
label.wrap = labelWidth > 0f
if (labelWidth == 0f)
table.add(label)
else
table.add(label).width(labelWidth)
}
return table
}
}
/** Makes [renderer][render] available outside [ICivilopediaText] */
object MarkupRenderer {
private const val emptyLineHeight = 10f
private const val defaultPadding = 2.5f
/**
* 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 linkAction Delegate to call for internal links. Leave null to suppress linking.
*/
fun render(
lines: Collection<FormattedLine>,
labelWidth: Float = 0f,
linkAction: ((id: String) -> Unit)? = null
): Table {
val skin = CameraStageBaseScreen.skin
val table = Table(skin).apply { defaults().pad(defaultPadding).align(Align.left) }
for (line in lines) {
if (line.isEmpty()) {
table.add().padTop(emptyLineHeight).row()
continue
}
val actor = line.render(labelWidth)
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).row()
else
table.add(actor).width(labelWidth).row()
}
return table.apply { pack() }
}
}

View File

@ -471,8 +471,15 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
}
var blinkAction: Action? = null
fun setCenterPosition(vector: Vector2, immediately: Boolean = false, selectUnit: Boolean = true) {
val tileGroup = allWorldTileGroups.firstOrNull { it.tileInfo.position == vector } ?: return
/** Scrolls the world map to specified coordinates.
* @param vector Position to center on
* @param immediately Do so without animation
* @param selectUnit Select a unit at the destination
* @return `true` if scroll position was changed, `false` otherwise
*/
fun setCenterPosition(vector: Vector2, immediately: Boolean = false, selectUnit: Boolean = true): Boolean {
val tileGroup = allWorldTileGroups.firstOrNull { it.tileInfo.position == vector } ?: return false
selectedTile = tileGroup.tileInfo
if (selectUnit)
worldScreen.bottomUnitTable.tileSelected(selectedTile!!)
@ -487,6 +494,8 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
// Here it's the same, only the Y axis is inverted - when at 0 we're at the top, not bottom - so we invert it back.
val finalScrollY = maxY - (tileGroup.y + tileGroup.width / 2 - height / 2)
if (finalScrollX == originalScrollX && finalScrollY == originalScrollY) return false
if (immediately) {
scrollX = finalScrollX
scrollY = finalScrollY
@ -513,6 +522,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
addAction(blinkAction) // Don't set it on the group because it's an actionlss group
worldScreen.shouldUpdate = true
return true
}
override fun zoom(zoomScale: Float) {

View File

@ -6,6 +6,8 @@ import com.badlogic.gdx.utils.Align
import com.unciv.UncivGame
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.TileInfo
import com.unciv.ui.civilopedia.CivilopediaScreen
import com.unciv.ui.civilopedia.MarkupRenderer
import com.unciv.ui.utils.CameraStageBaseScreen
import com.unciv.ui.utils.ImageGetter
import com.unciv.ui.utils.toLabel
@ -20,7 +22,9 @@ class TileInfoTable(private val viewingCiv :CivilizationInfo) : Table(CameraStag
if (tile != null && (UncivGame.Current.viewEntireMapForDebug || viewingCiv.exploredTiles.contains(tile.position)) ) {
add(getStatsTable(tile))
add(tile.toString(viewingCiv).toLabel()).colspan(2).pad(10f)
add( MarkupRenderer.render(tile.toMarkup(viewingCiv) ) {
UncivGame.Current.setScreen(CivilopediaScreen(viewingCiv.gameInfo.ruleSet, link = it))
} ).pad(10f)
// For debug only!
// add(tile.position.toString().toLabel()).colspan(2).pad(10f)
}
@ -40,4 +44,4 @@ class TileInfoTable(private val viewingCiv :CivilizationInfo) : Table(CameraStag
}
return table
}
}
}

View File

@ -13,6 +13,8 @@ import com.unciv.logic.city.CityInfo
import com.unciv.logic.map.MapUnit
import com.unciv.logic.map.TileInfo
import com.unciv.models.translations.tr
import com.unciv.ui.civilopedia.CivilopediaCategories
import com.unciv.ui.civilopedia.CivilopediaScreen
import com.unciv.ui.pickerscreens.PromotionPickerScreen
import com.unciv.ui.utils.*
import com.unciv.ui.worldscreen.WorldScreen
@ -78,7 +80,9 @@ class UnitTable(val worldScreen: WorldScreen) : Table(){
touchable = Touchable.enabled
onClick {
selectedUnit?.currentTile?.position?.let {
worldScreen.mapHolder.setCenterPosition(it, false, false)
if ( !worldScreen.mapHolder.setCenterPosition(it, false, false) && selectedUnit != null ) {
worldScreen.game.setScreen(CivilopediaScreen(worldScreen.gameInfo.ruleSet, CivilopediaCategories.Unit, selectedUnit!!.name))
}
}
}
}).expand()