Upgrading from Unit overview improved (#9485)

* Unit upgrade tooltip in overview

* Unit upgrade tooltip in action table

* Unit upgrade tooltip in action table - colored Key

* Unit upgrade in Overview - reselect

* Fix merge problems and FormattedLine color markup ability

* Relax MarkupRenderer.render lines parameter type

* Skin has a getColor shortcut - use it

* Unit overview upgrade icons now open a menu instead of upgrading immediately

* Unit Overview upgrade - "Mid" buttons

* Unit Overview upgrade - reorg
This commit is contained in:
SomeTroglodyte 2023-05-31 17:41:57 +02:00 committed by GitHub
parent b851abc7fd
commit fcd309781d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 462 additions and 98 deletions

View File

@ -299,6 +299,8 @@ Non-existent city =
Lost ability =
National ability =
[firstValue] vs [secondValue] =
Gained =
Lost =
# New game screen

View File

@ -3,6 +3,7 @@ package com.unciv.models
import com.badlogic.gdx.Input
import com.badlogic.gdx.scenes.scene2d.Actor
import com.unciv.Constants
import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.translations.getPlaceholderParameters
import com.unciv.ui.components.Fonts
import com.unciv.ui.components.KeyCharAndCode
@ -11,9 +12,9 @@ import com.unciv.ui.images.ImageGetter
/** Unit Actions - class - carries dynamic data and actual execution.
* Static properties are in [UnitActionType].
* Note this is for the buttons offering actions, not the ongoing action stored with a [MapUnit][com.unciv.logic.map.MapUnit]
* Note this is for the buttons offering actions, not the ongoing action stored with a [MapUnit][com.unciv.logic.map.mapunit.MapUnit]
*/
data class UnitAction(
open class UnitAction(
val type: UnitActionType,
val title: String = type.value,
val isCurrentAction: Boolean = false,
@ -38,8 +39,44 @@ data class UnitAction(
else -> ImageGetter.getUnitActionPortrait("Star")
}
}
//TODO remove once sure they're unused
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is UnitAction) return false
if (type != other.type) return false
if (isCurrentAction != other.isCurrentAction) return false
if (action != other.action) return false
return true
}
override fun hashCode(): Int {
var result = type.hashCode()
result = 31 * result + isCurrentAction.hashCode()
result = 31 * result + (action?.hashCode() ?: 0)
return result
}
override fun toString(): String {
return "UnitAction(type=$type, title='$title', isCurrentAction=$isCurrentAction)"
}
}
/** Specialized [UnitAction] for upgrades
*
* Transports [unitToUpgradeTo] from [creation][com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade.getUpgradeAction]
* to [UI][com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsTable.update]
*/
class UpgradeUnitAction(
title: String,
val unitToUpgradeTo: BaseUnit,
val goldCostOfUpgrade: Int,
val newResourceRequirements: Counter<String>,
action: (() -> Unit)?
) : UnitAction(UnitActionType.Upgrade, title, action = action)
/** Unit Actions - generic enum with static properties
*
* @param value _default_ label to display, can be overridden in UnitAction instantiation

View File

@ -13,6 +13,7 @@ import com.unciv.ui.components.Fonts
import com.unciv.ui.components.extensions.colorFromRGB
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen.Companion.showReligionInCivilopedia
import com.unciv.ui.screens.civilopediascreen.FormattedLine
import com.unciv.ui.objectdescriptions.BaseUnitDescriptions
import kotlin.math.pow
class Nation : RulesetObject() {
@ -230,42 +231,10 @@ class Nation : RulesetObject() {
yield(FormattedLine("Replaces [${originalUnit.name}]", link="Unit/${originalUnit.name}", indent=1))
if (unit.cost != originalUnit.cost)
yield(FormattedLine("{Cost} ".tr() + "[${unit.cost}] vs [${originalUnit.cost}]".tr(), indent=1))
if (unit.strength != originalUnit.strength)
yield(FormattedLine("${Fonts.strength} " + "[${unit.strength}] vs [${originalUnit.strength}]".tr(), indent=1))
if (unit.rangedStrength != originalUnit.rangedStrength)
yield(FormattedLine("${Fonts.rangedStrength} " + "[${unit.rangedStrength}] vs [${originalUnit.rangedStrength}]".tr(), indent=1))
if (unit.range != originalUnit.range)
yield(FormattedLine("${Fonts.range} " + "[${unit.range}] vs [${originalUnit.range}]".tr(), indent=1))
if (unit.movement != originalUnit.movement)
yield(FormattedLine("${Fonts.movement} " + "[${unit.movement}] vs [${originalUnit.movement}]".tr(), indent=1))
for (resource in originalUnit.getResourceRequirementsPerTurn().keys)
if (!unit.getResourceRequirementsPerTurn().containsKey(resource)) {
yield(FormattedLine("[$resource] not required", link="Resource/$resource", indent=1))
}
// This does not use the auto-linking FormattedLine(Unique) for two reasons:
// would look a little chaotic as unit uniques unlike most uniques are a HashSet and thus do not preserve order
// No .copy() factory on FormattedLine and no FormattedLine(Unique, all other val's) constructor either
if (unit.replacementTextForUniques.isNotEmpty()) {
yield(FormattedLine(unit.replacementTextForUniques))
}
else for (unique in unit.uniqueObjects.filterNot { it.text in originalUnit.uniques || it.hasFlag(UniqueFlag.HiddenToUsers) }) {
yield(FormattedLine(unique.text.tr(), indent = 1))
}
for (unique in originalUnit.uniqueObjects.filterNot { it.text in unit.uniques || it.hasFlag(UniqueFlag.HiddenToUsers) }) {
yield(
FormattedLine("Lost ability".tr() + " (" + "vs [${originalUnit.name}]".tr() + "): " +
unique.text.tr(), indent = 1)
)
}
for (promotion in unit.promotions.filter { it !in originalUnit.promotions }) {
val effect = ruleset.unitPromotions[promotion]!!.uniques
// "{$promotion} ({$effect})" won't work as effect may contain [] and tr() does not support that kind of nesting
yield(
FormattedLine(
"${promotion.tr(true)} (${effect.joinToString(",") { it.tr() }})",
link = "Promotion/$promotion", indent = 1 )
)
}
yieldAll(
BaseUnitDescriptions.getDifferences(ruleset, originalUnit, unit)
.map { (text, link) -> FormattedLine(text, link = link ?: "", indent = 1) }
)
} else if (unit.replaces != null) {
yield(FormattedLine("Replaces [${unit.replaces}], which is not found in the ruleset!", indent = 1))
} else {

View File

@ -12,7 +12,8 @@ import com.unciv.ui.screens.basescreen.BaseScreen
/** A Label allowing Gdx markup
*
* See also [Color Markup Language](https://libgdx.com/wiki/graphics/2d/fonts/color-markup-language)
* This constructor does _not_ auto-translate or otherwise preprocess [text]
* See also [Color Markup Language](https://libgdx.com/wiki/graphics/2d/fonts/color-markup-language)
*/
class ColorMarkupLabel private constructor(
fontSize: Int, // inverted order so it can be differentiated from the translating constructor
@ -22,18 +23,31 @@ class ColorMarkupLabel private constructor(
/** A Label allowing Gdx markup, auto-translated.
*
* Since Gdx markup markers are interpreted and removed by translation, use «» instead.
*
* @param defaultColor the color text starts with - will be converted to markup, not actor tint
* @param hideIcons passed to translation to prevent auto-insertion of symbols for gameplay names
*/
constructor(text: String, fontSize: Int = Constants.defaultFontSize)
: this(fontSize, mapMarkup(text))
constructor(
text: String,
fontSize: Int = Constants.defaultFontSize,
defaultColor: Color = Color.WHITE,
hideIcons: Boolean = false
) : this(fontSize, mapMarkup(text, defaultColor, hideIcons))
/** A Label automatically applying Gdx markup colors to symbols and rest of text separately
* - _after_ translating [text].
/** A Label automatically applying Gdx markup colors to symbols and rest of text separately -
* _**after**_ translating [text].
*
* Use to easily color text without also coloring the icons which translation inserts as
* characters for recognized gameplay names.
*
* @see Fonts.charToRulesetImageActor
*/
constructor(text: String,
textColor: Color,
symbolColor: Color = Color.WHITE,
fontSize: Int = Constants.defaultFontSize)
: this (fontSize, prepareText(text, textColor, symbolColor))
constructor(
text: String,
textColor: Color,
symbolColor: Color = Color.WHITE,
fontSize: Int = Constants.defaultFontSize
) : this (fontSize, prepareText(text, textColor, symbolColor))
/** Only if wrap was turned on, this is the prefWidth before.
* Used for getMaxWidth as better estimate than the default 0. */
@ -88,12 +102,6 @@ class ColorMarkupLabel private constructor(
override fun getMaxWidth() = unwrappedPrefWidth // If unwrapped, we return 0 same as super
companion object {
private fun mapMarkup(text: String): String {
val translated = text.tr()
if ('«' !in translated) return translated
return translated.replace('«', '[').replace('»', ']')
}
private val inverseColorMap = Colors.getColors().associate { it.value to it.key }
private fun Color.toMarkup(): String {
val mapEntry = inverseColorMap[this]
@ -102,9 +110,16 @@ class ColorMarkupLabel private constructor(
return "#" + toString().substring(0,6)
}
private fun mapMarkup(text: String, defaultColor: Color, hideIcons: Boolean): String {
val translated = if (defaultColor == Color.WHITE) text.tr(hideIcons)
else "[${defaultColor.toMarkup()}]${text.tr(hideIcons)}[]"
if ('«' !in translated) return translated
return translated.replace('«', '[').replace('»', ']')
}
private fun prepareText(text: String, textColor: Color, symbolColor: Color): String {
val translated = text.tr()
if (textColor == Color.WHITE && symbolColor == Color.WHITE || translated.isBlank())
if ((textColor == Color.WHITE && symbolColor == Color.WHITE) || translated.isBlank())
return translated
val tc = textColor.toMarkup()
if (textColor == symbolColor)

View File

@ -429,7 +429,7 @@ fun Group.addBorderAllowOpacity(size:Float, color: Color): Group {
/** get background Image for a new separator */
private fun getSeparatorImage(color: Color) = ImageGetter.getDot(
if (color.a != 0f) color else BaseScreen.skin.get("color", Color::class.java) //0x334d80
if (color.a != 0f) color else BaseScreen.skin.getColor("color") //0x334d80
)
/**

View File

@ -1,7 +1,13 @@
package com.unciv.ui.objectdescriptions
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.ui.Container
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup
import com.unciv.logic.city.City
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueFlag
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit
@ -13,6 +19,8 @@ import com.unciv.ui.components.Fonts
import com.unciv.ui.components.extensions.getConsumesAmountString
import com.unciv.ui.components.extensions.toPercent
import com.unciv.ui.screens.civilopediascreen.FormattedLine
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.civilopediascreen.MarkupRenderer
import kotlin.math.pow
object BaseUnitDescriptions {
@ -196,6 +204,7 @@ object BaseUnitDescriptions {
return textList
}
@Suppress("RemoveExplicitTypeArguments") // for faster IDE - inferring sequence types can be slow
fun UnitType.getUnitTypeCivilopediaTextLines(ruleset: Ruleset): List<FormattedLine> {
fun getDomainLines() = sequence<FormattedLine> {
yield(FormattedLine("{Unit types}:", header = 4))
@ -231,4 +240,84 @@ object BaseUnitDescriptions {
}
return (if (name.startsWith("Domain: ")) getDomainLines() else getUnitTypeLines()).toList()
}
/**
* Lists differences e.g. for help on an upgrade, or how a nation-unique compares to its replacement.
*
* Cost is **not** included.
* Result lines are **not** translated.
*
* @param originalUnit The "older" unit
* @param betterUnit The "newer" unit
* @return Sequence of Pairs - first is the actual text, second is an optional link for Civilopedia use
*/
fun getDifferences(ruleset: Ruleset, originalUnit: BaseUnit, betterUnit: BaseUnit):
Sequence<Pair<String, String?>> = sequence {
if (betterUnit.strength != originalUnit.strength)
yield("${Fonts.strength} {[${betterUnit.strength}] vs [${originalUnit.strength}]}" to null)
if (betterUnit.rangedStrength > 0 && originalUnit.rangedStrength == 0)
yield("[Gained] ${Fonts.rangedStrength} [${betterUnit.rangedStrength}] ${Fonts.range} [${betterUnit.range}]" to null)
else if (betterUnit.rangedStrength == 0 && originalUnit.rangedStrength > 0)
yield("[Lost] ${Fonts.rangedStrength} [${originalUnit.rangedStrength}] ${Fonts.range} [${originalUnit.range}]" to null)
else {
if (betterUnit.rangedStrength != originalUnit.rangedStrength)
yield("${Fonts.rangedStrength} " + "[${betterUnit.rangedStrength}] vs [${originalUnit.rangedStrength}]" to null)
if (betterUnit.range != originalUnit.range)
yield("${Fonts.range} {[${betterUnit.range}] vs [${originalUnit.range}]}" to null)
}
if (betterUnit.movement != originalUnit.movement)
yield("${Fonts.movement} {[${betterUnit.movement}] vs [${originalUnit.movement}]}" to null)
for (resource in originalUnit.getResourceRequirementsPerTurn().keys)
if (!betterUnit.getResourceRequirementsPerTurn().containsKey(resource)) {
yield("[$resource] not required" to "Resource/$resource")
}
// We return the unique text directly, so Nation.getUniqueUnitsText will not use the
// auto-linking FormattedLine(Unique) - two reasons in favor:
// would look a little chaotic as unit uniques unlike most uniques are a HashSet and thus do not preserve order
// No .copy() factory on FormattedLine and no (Unique, all other val's) constructor either
if (betterUnit.replacementTextForUniques.isNotEmpty()) {
yield(betterUnit.replacementTextForUniques to null)
} else {
val newAbilityPredicate: (Unique)->Boolean = { it.text in originalUnit.uniques || it.hasFlag(UniqueFlag.HiddenToUsers) }
for (unique in betterUnit.uniqueObjects.filterNot(newAbilityPredicate))
yield(unique.text to null)
}
val lostAbilityPredicate: (Unique)->Boolean = { it.text in betterUnit.uniques || it.hasFlag(UniqueFlag.HiddenToUsers) }
for (unique in originalUnit.uniqueObjects.filterNot(lostAbilityPredicate)) {
yield("Lost ability (vs [${originalUnit.name}]): [${unique.text}]" to null)
}
for (promotion in betterUnit.promotions.filter { it !in originalUnit.promotions }) {
val effects = ruleset.unitPromotions[promotion]!!.uniques
.joinToString(",") { "{$it}" } // {} for individual translations, default separator would have extra blank
yield("{$promotion} ($effects)" to "Promotion/$promotion")
}
}
/** Prepares a WidgetGroup for display as tooltip to an upgrade
* Specialized to the WorldScreen UnitAction button and Unit Overview upgrade icon -
* in both cases the [UnitAction][com.unciv.models.UnitAction] and [unitToUpgradeTo] have already been evaluated.
*/
fun getUpgradeTooltipActor(title: String, unitUpgrading: BaseUnit, unitToUpgradeTo: BaseUnit) =
getUpgradeInfoTable(title, unitUpgrading, unitToUpgradeTo).wrapScaled(0.667f)
fun getUpgradeInfoTable(title: String, unitUpgrading: BaseUnit, unitToUpgradeTo: BaseUnit): Table {
val ruleset = unitToUpgradeTo.ruleset
val info = sequenceOf(FormattedLine(title, color = "#FDA", icon = unitToUpgradeTo.makeLink(), header = 5)) +
getDifferences(ruleset, unitUpgrading, unitToUpgradeTo)
.map { FormattedLine(it.first, icon = it.second ?: "") }
val infoTable = MarkupRenderer.render(info.asIterable(), 400f)
infoTable.background = BaseScreen.skinStrings.getUiBackground("General/Tooltip", BaseScreen.skinStrings.roundedEdgeRectangleShape, Color.DARK_GRAY)
return infoTable
}
private fun Table.wrapScaled(scale: Float): WidgetGroup =
Container(this).apply {
touchable = Touchable.disabled
isTransform = true
setScale(scale)
}
}

View File

@ -14,5 +14,5 @@ fun aboutTab(): Table {
yield(FormattedLine("See online Readme", link = "https://github.com/yairm210/Unciv/blob/master/README.md#unciv---foss-civ-v-for-androiddesktop"))
yield(FormattedLine("Visit repository", link = "https://github.com/yairm210/Unciv"))
}
return MarkupRenderer.render(lines.toList()).pad(20f)
return MarkupRenderer.render(lines.asIterable()).pad(20f)
}

View File

@ -16,8 +16,8 @@ class CitizenManagementTable(val cityScreen: CityScreen) : Table(BaseScreen.skin
fun update() {
clear()
val colorSelected = BaseScreen.skin.get("selection", Color::class.java)
val colorButton = BaseScreen.skin.get("color", Color::class.java)
val colorSelected = BaseScreen.skin.getColor("selection")
val colorButton = BaseScreen.skin.getColor("color")
// effectively a button, but didn't want to rewrite TextButton style
// and much more compact and can control backgrounds easily based on settings
val resetLabel = "Reset Citizens".toLabel()

View File

@ -77,7 +77,7 @@ class CityStatsTable(private val cityScreen: CityScreen): Table() {
lowerTable.clear()
val miniStatsTable = Table()
val selected = BaseScreen.skin.get("selection", Color::class.java)
val selected = BaseScreen.skin.getColor("selection")
for ((stat, amount) in cityInfo.cityStats.currentCityStats) {
if (stat == Stat.Faith && !cityInfo.civ.gameInfo.isReligionEnabled()) continue
val icon = Table()

View File

@ -27,7 +27,7 @@ class SpecialistAllocationTable(private val cityScreen: CityScreen) : Table(Base
if (cityScreen.canCityBeChanged()) {
if (cityInfo.manualSpecialists) {
val manualSpecialists = "Manual Specialists".toLabel()
.addBorder(5f, BaseScreen.skin.get("color", Color::class.java))
.addBorder(5f, BaseScreen.skin.getColor("color"))
manualSpecialists.onClick {
cityInfo.manualSpecialists = false
cityInfo.reassignPopulation(); cityScreen.update()
@ -35,7 +35,7 @@ class SpecialistAllocationTable(private val cityScreen: CityScreen) : Table(Base
add(manualSpecialists).colspan(5).row()
} else {
val autoSpecialists = "Auto Specialists".toLabel()
.addBorder(5f, BaseScreen.skin.get("color", Color::class.java))
.addBorder(5f, BaseScreen.skin.getColor("color"))
autoSpecialists.onClick { cityInfo.manualSpecialists = true; update() }
add(autoSpecialists).colspan(5).row()
}

View File

@ -11,9 +11,11 @@ import com.unciv.models.metadata.BaseRuleset
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.unique.Unique
import com.unciv.ui.components.ColorMarkupLabel
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.components.extensions.toLabel
import com.unciv.utils.Log
import kotlin.math.max
@ -258,7 +260,7 @@ class FormattedLine (
size == Int.MIN_VALUE -> Constants.defaultFontSize
else -> size
}
val labelColor = if(starred) defaultColor else displayColor
val labelColor = if (starred) defaultColor else displayColor
val table = Table(BaseScreen.skin)
var iconCount = 0
@ -285,7 +287,10 @@ class FormattedLine (
else -> (indent-1) * indentPad +
indentOneAtNumIcons * (minIconSize + iconPad) + iconPad - usedWidth
}
val label = textToDisplay.toLabel(labelColor, fontSize, hideIcons = iconCount!=0)
val label = if ('«' in textToDisplay)
ColorMarkupLabel(textToDisplay, fontSize, hideIcons = iconCount != 0)
else
textToDisplay.toLabel(labelColor, fontSize, hideIcons = iconCount != 0)
label.wrap = !centered && labelWidth > 0f
label.setAlignment(align)
if (labelWidth == 0f)

View File

@ -28,7 +28,7 @@ object MarkupRenderer {
* @param linkAction Delegate to call for internal links. Leave null to suppress linking.
*/
fun render(
lines: Collection<FormattedLine>,
lines: Iterable<FormattedLine>,
labelWidth: Float = 0f,
padding: Float = defaultPadding,
iconDisplay: FormattedLine.IconDisplay = FormattedLine.IconDisplay.All,

View File

@ -49,7 +49,7 @@ class MapEditorToolsDrawer(
add(arrowWrapper).align(Align.center).width(handleWidth).fillY().apply { // the "handle"
background = BaseScreen.skinStrings.getUiBackground(
"MapEditor/MapEditorToolsDrawer/Handle",
tintColor = BaseScreen.skin.get("color", Color::class.java)
tintColor = BaseScreen.skin.getColor("color")
)
}

View File

@ -59,7 +59,7 @@ class MapEditorEditTerrainTab(
.filter { it.type.isBaseTerrain }
private fun getTerrains() = allTerrains()
.map { FormattedLine(it.name, it.name, "Terrain/${it.name}", size = 32) }
.toList()
.asIterable()
override fun isDisabled() = false // allTerrains().none() // wanna see _that_ mod...
}
@ -100,7 +100,7 @@ class MapEditorEditFeaturesTab(
.filter { it.type == TerrainType.TerrainFeature }
private fun getFeatures() = allowedFeatures()
.map { FormattedLine(it.name, it.name, "Terrain/${it.name}", size = 32) }
.toList()
.asIterable()
override fun isDisabled() = allowedFeatures().none()
}
@ -133,7 +133,7 @@ class MapEditorEditWondersTab(
.filter { it.type == TerrainType.NaturalWonder }
private fun getWonders() = allowedWonders()
.map { FormattedLine(it.name, it.name, "Terrain/${it.name}", size = 32) }
.toList()
.asIterable()
override fun isDisabled() = allowedWonders().none()
}
@ -176,7 +176,7 @@ class MapEditorEditResourcesTab(
private fun allowedResources() = ruleset.tileResources.values.asSequence()
.filter { !it.hasUnique(UniqueType.CityStateOnlyResource) }
private fun getResources(): List<FormattedLine> = sequence {
private fun getResources(): Iterable<FormattedLine> = sequence {
var lastGroup = ResourceType.Bonus
for (resource in allowedResources()) {
val name = resource.name
@ -186,7 +186,7 @@ class MapEditorEditResourcesTab(
}
yield (FormattedLine(name, name, "Resource/$name", size = 32))
}
}.toList()
}.asIterable()
override fun isDisabled() = allowedResources().none()
}
@ -233,7 +233,7 @@ class MapEditorEditImprovementsTab(
.filter { improvement ->
disallowImprovements.none { improvement.name.startsWith(it) }
}
private fun getImprovements(): List<FormattedLine> = sequence {
private fun getImprovements(): Iterable<FormattedLine> = sequence {
var lastGroup = 0
for (improvement in allowedImprovements()) {
val name = improvement.name
@ -244,7 +244,7 @@ class MapEditorEditImprovementsTab(
}
yield (FormattedLine(name, name, "Improvement/$name", size = 32))
}
}.toList()
}.asIterable()
override fun isDisabled() = allowedImprovements().none()
@ -306,7 +306,7 @@ class MapEditorEditStartsTab(
private fun getNations() = allowedNations()
.sortedWith(compareBy<Nation>{ it.isCityState }.thenBy(collator) { it.name.tr() })
.map { FormattedLine("[${it.name}] starting location", it.name, "Nation/${it.name}", size = 24) }
.toList()
.asIterable()
override fun isDisabled() = allowedNations().none()

View File

@ -138,7 +138,7 @@ class MapEditorViewTab(
startsOutOpened = false,
headerPad = 5f
) {
it.add(MarkupRenderer.render(lines.toList(), iconDisplay = IconDisplay.NoLink) { name ->
it.add(MarkupRenderer.render(lines.asIterable(), iconDisplay = IconDisplay.NoLink) { name ->
scrollToStartOfNation(name)
})
}).row()

View File

@ -14,7 +14,8 @@ import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile
import com.unciv.models.UnitActionType
import com.unciv.ui.audio.SoundPlayer
import com.unciv.models.UpgradeUnitAction
import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.ui.components.ExpanderTab
import com.unciv.ui.components.Fonts
import com.unciv.ui.components.TabbedPager
@ -273,16 +274,20 @@ class UnitOverviewTab(
add(promotionsTable)
// Upgrade column
if (unit.upgrade.canUpgrade()) {
val unitAction = UnitActionsUpgrade.getUpgradeAction(unit)
val enable = unitAction?.action != null && viewingPlayer.isCurrentPlayer() &&
val unitAction = UnitActionsUpgrade.getUpgradeActionAnywhere(unit)
if (unitAction != null) {
val enable = unitAction.action != null && viewingPlayer.isCurrentPlayer() &&
GUI.isAllowedChangeState()
val upgradeIcon = ImageGetter.getUnitIcon(unit.upgrade.getUnitToUpgradeTo().name,
val unitToUpgradeTo = (unitAction as UpgradeUnitAction).unitToUpgradeTo
val selectKey = getUnitIdentifier(unit, unitToUpgradeTo)
val upgradeIcon = ImageGetter.getUnitIcon(unitToUpgradeTo.name,
if (enable) Color.GREEN else Color.GREEN.darken(0.5f))
if (enable) upgradeIcon.onClick {
SoundPlayer.play(unitAction!!.uncivSound)
unitAction.action!!()
unitListTable.updateUnitListTable()
val pos = upgradeIcon.localToStageCoordinates(Vector2(upgradeIcon.width/2, upgradeIcon.height/2))
UnitUpgradeMenu(overviewScreen.stage, pos, unit, unitAction) {
unitListTable.updateUnitListTable()
select(selectKey)
}
}
add(upgradeIcon).size(28f)
} else add()
@ -295,7 +300,10 @@ class UnitOverviewTab(
}
companion object {
fun getUnitIdentifier(unit: MapUnit) = unit.run { "$name@${getTile().position.toPrettyString()}" }
fun getUnitIdentifier(unit: MapUnit, unitToUpgradeTo: BaseUnit? = null): String {
val name = unitToUpgradeTo?.name ?: unit.name
return "$name@${unit.getTile().position.toPrettyString()}"
}
}
override fun select(selection: String): Float? {
@ -316,4 +324,5 @@ class UnitOverviewTab(
button.addAction(blinkAction)
return scrollY
}
}

View File

@ -0,0 +1,209 @@
package com.unciv.ui.screens.overviewscreen
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.g2d.NinePatch
import com.badlogic.gdx.math.Interpolation
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.scenes.scene2d.Stage
import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.actions.Actions
import com.badlogic.gdx.scenes.scene2d.ui.Container
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.scenes.scene2d.utils.NinePatchDrawable
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.models.UpgradeUnitAction
import com.unciv.ui.audio.SoundPlayer
import com.unciv.ui.components.KeyCharAndCode
import com.unciv.ui.components.KeyboardBinding
import com.unciv.ui.components.extensions.keyShortcuts
import com.unciv.ui.components.extensions.onActivation
import com.unciv.ui.components.extensions.pad
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.objectdescriptions.BaseUnitDescriptions
import com.unciv.ui.popups.Popup
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade
//TODO When this gets reused. e.g. from UnitActionsTable, move to another package.
/**
* A popup menu showing info about an Unit upgrade, with buttons to upgrade "this" unit or _all_
* similar units.
*
* Meant to animate "in" at a given position - unlike other [Popup]s which are always stage-centered.
* No close button - use "click-behind".
* The "click-behind" semi-transparent covering of the rest of the stage is much darker than a normal
* Popup (geve the impression to take away illumination and spotlight the menu) and fades in together
* with the UnitUpgradeMenu itself. Closing the menu in any of the four ways will fade out everything
* inverting the fade-and-scale-in.
*
* @param stage The stage this will be shown on, passed to Popup and used for clamping **`position`**
* @param position stage coortinates to show this centered over - clamped so that nothing is clipped outside the [stage]
* @param unit Who is ready to upgrade?
* @param unitAction Holds pre-calculated info like unitToUpgradeTo, cost or resource requirements. Its action is mapped to the Upgrade button.
* @param onButtonClicked A callback after one or several upgrades have been performed (and the menu is about to close)
*/
class UnitUpgradeMenu(
stage: Stage,
position: Vector2,
private val unit: MapUnit,
private val unitAction: UpgradeUnitAction,
private val onButtonClicked: () -> Unit
) : Popup(stage, scrollable = false) {
private val container: Container<Table>
private val allUpgradableUnits: Sequence<MapUnit>
private val animationDuration = 0.33f
private val backgroundColor = (background as NinePatchDrawable).patch.color
init {
innerTable.remove()
// Note: getUpgradeInfoTable skins this as General/Tooltip, roundedEdgeRectangle, DARK_GRAY
// TODO - own skinnable path, possibly when tooltip use of getUpgradeInfoTable gets replaced
val newInnerTable = BaseUnitDescriptions.getUpgradeInfoTable(
unitAction.title, unit.baseUnit, unitAction.unitToUpgradeTo
)
newInnerTable.row()
val smallButtonStyle = SmallButtonStyle()
val upgradeButton = "Upgrade".toTextButton(smallButtonStyle)
upgradeButton.onActivation(::doUpgrade)
upgradeButton.keyShortcuts.add(KeyboardBinding.Confirm)
newInnerTable.add(upgradeButton).pad(15f, 15f, 5f, 15f).growX().row()
allUpgradableUnits = unit.civ.units.getCivUnits()
.filter {
it.baseUnit.name == unit.baseUnit.name
&& it.currentMovement > 0f
&& it.currentTile.getOwner() == unit.civ
&& !it.isEmbarked()
&& it.upgrade.canUpgrade(unitAction.unitToUpgradeTo, ignoreResources = true)
}
newInnerTable.tryAddUpgradeAllUnitsButton(smallButtonStyle)
clickBehindToClose = true
keyShortcuts.add(KeyCharAndCode.BACK) { close() }
newInnerTable.pack()
container = Container(newInnerTable)
container.touchable = Touchable.childrenOnly
container.isTransform = true
container.setScale(0.05f)
container.color.a = 0f
open(true) // this only does the screen-covering "click-behind" portion
container.setPosition(
position.x.coerceAtMost(stage.width - newInnerTable.width / 2),
position.y.coerceAtLeast(newInnerTable.height / 2)
)
addActor(container)
container.addAction(
Actions.parallel(
Actions.scaleTo(1f, 1f, animationDuration, Interpolation.fade),
Actions.fadeIn(animationDuration, Interpolation.fade)
))
backgroundColor.set(0)
addAction(Actions.alpha(0.35f, animationDuration, Interpolation.fade).apply {
color = backgroundColor
})
}
private fun Table.tryAddUpgradeAllUnitsButton(buttonStyle: TextButton.TextButtonStyle) {
val allCount = allUpgradableUnits.count()
if (allCount <= 1) return
// Note - all same-baseunit units cost the same to upgrade? What if a mod says e.g. 50% discount on Oasis?
// - As far as I can see the rest of the upgrading code doesn't support such conditions at the moment.
val allCost = unitAction.goldCostOfUpgrade * allCount
val allResources = unitAction.newResourceRequirements * allCount
val upgradeAllButton = "Upgrade all [$allCount] [${unit.name}] ([$allCost] gold)"
.toTextButton(buttonStyle)
upgradeAllButton.isDisabled = unit.civ.gold < allCost ||
allResources.isNotEmpty() &&
unit.civ.getCivResourcesByName().run {
allResources.any {
it.value > (this[it.key] ?: 0)
}
}
upgradeAllButton.onActivation(::doAllUpgrade)
add(upgradeAllButton).pad(2f, 15f).growX().row()
}
private fun doUpgrade() {
SoundPlayer.play(unitAction.uncivSound)
unitAction.action!!()
onButtonClicked()
close()
}
private fun doAllUpgrade() {
stage.addAction(
Actions.sequence(
Actions.run { SoundPlayer.play(unitAction.uncivSound) },
Actions.delay(0.2f),
Actions.run { SoundPlayer.play(unitAction.uncivSound) }
))
for (unit in allUpgradableUnits) {
val otherAction = UnitActionsUpgrade.getUpgradeAction(unit)
otherAction?.action?.invoke()
}
onButtonClicked()
close()
}
override fun close() {
addAction(Actions.alpha(0f, animationDuration, Interpolation.fade).apply {
color = backgroundColor
})
container.addAction(
Actions.sequence(
Actions.parallel(
Actions.scaleTo(0.05f, 0.05f, animationDuration, Interpolation.fade),
Actions.fadeOut(animationDuration, Interpolation.fade)
),
Actions.run {
container.remove()
super.close()
}
))
}
class SmallButtonStyle : TextButton.TextButtonStyle(BaseScreen.skin[TextButton.TextButtonStyle::class.java]) {
/** Modify NinePatch geometry so the roundedEdgeRectangleMidShape button is 38f high instead of 48f,
* Otherwise this excercise would be futile - normal roundedEdgeRectangleShape based buttons are 50f high.
*/
private fun NinePatchDrawable.reduce(): NinePatchDrawable {
val patch = NinePatch(this.patch)
patch.padTop = 10f
patch.padBottom = 10f
patch.topHeight = 10f
patch.bottomHeight = 10f
return NinePatchDrawable(this).also { it.patch = patch }
}
init {
val upColor = BaseScreen.skin.getColor("color")
val downColor = BaseScreen.skin.getColor("pressed")
val overColor = BaseScreen.skin.getColor("highlight")
val disabledColor = BaseScreen.skin.getColor("disabled")
// UiElementDocsWriter inspects source, which is why this isn't prettified better
val shape = BaseScreen.run {
// Let's use _one_ skinnable background lookup but with different tints
val skinned = skinStrings.getUiBackground("UnitUpgradeMenu/Button", skinStrings.roundedEdgeRectangleMidShape)
// Reduce height only if not skinned
val default = ImageGetter.getNinePatch(skinStrings.roundedEdgeRectangleMidShape)
if (skinned === default) default.reduce() else skinned
}
// Now get the tinted variants
up = shape.tint(upColor)
down = shape.tint(downColor)
over = shape.tint(overColor)
disabled = shape.tint(disabledColor)
disabledFontColor = Color.GRAY
}
}
}

View File

@ -1,19 +1,25 @@
package com.unciv.ui.screens.worldscreen.unit.actions
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.scenes.scene2d.ui.Button
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.unciv.GUI
import com.unciv.UncivGame
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.models.UnitAction
import com.unciv.models.UnitActionType
import com.unciv.models.UpgradeUnitAction
import com.unciv.ui.components.KeyCharAndCode
import com.unciv.ui.components.UncivTooltip
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
import com.unciv.ui.components.extensions.disable
import com.unciv.ui.components.extensions.keyShortcuts
import com.unciv.ui.components.extensions.onActivation
import com.unciv.ui.components.extensions.packIfNeeded
import com.unciv.ui.images.IconTextButton
import com.unciv.ui.objectdescriptions.BaseUnitDescriptions
import com.unciv.ui.screens.worldscreen.WorldScreen
class UnitActionsTable(val worldScreen: WorldScreen) : Table() {
@ -22,9 +28,17 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() {
clear()
if (unit == null) return
if (!worldScreen.canChangeState) return // No actions when it's not your turn or spectator!
for (button in UnitActions.getUnitActions(unit)
.map { getUnitActionButton(unit, it) })
for (unitAction in UnitActions.getUnitActions(unit)) {
val button = getUnitActionButton(unit, unitAction)
if (unitAction is UpgradeUnitAction) {
val tipTitle = "«RED»${unitAction.type.key}«»: {Upgrade}"
val tipActor = BaseUnitDescriptions.getUpgradeTooltipActor(tipTitle, unit.baseUnit, unitAction.unitToUpgradeTo)
button.addListener(UncivTooltip(button, tipActor
, offset = Vector2(0f, tipActor.packIfNeeded().height * 0.333f) // scaling fails to express size in parent coordinates
, tipAlign = Align.topLeft, targetAlign = Align.topRight))
}
add(button).left().padBottom(2f).row()
}
pack()
}
@ -40,7 +54,8 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() {
if (unitAction.type == UnitActionType.Promote && unitAction.action != null)
actionButton.color = Color.GREEN.cpy().lerp(Color.WHITE, 0.5f)
actionButton.addTooltip(key)
if (unitAction !is UpgradeUnitAction) // Does its own toolTip
actionButton.addTooltip(key)
actionButton.pack()
if (unitAction.action == null) {
actionButton.disable()

View File

@ -3,11 +3,11 @@ package com.unciv.ui.screens.worldscreen.unit.actions
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.models.Counter
import com.unciv.models.UnitAction
import com.unciv.models.UnitActionType
import com.unciv.models.UpgradeUnitAction
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.tr
object UnitActionsUpgrade{
object UnitActionsUpgrade {
internal fun addUnitUpgradeAction(
unit: MapUnit,
@ -21,13 +21,14 @@ object UnitActionsUpgrade{
private fun getUpgradeAction(
unit: MapUnit,
isFree: Boolean,
isSpecial: Boolean
isSpecial: Boolean,
isAnywhere: Boolean
): UnitAction? {
val specialUpgradesTo = unit.baseUnit().getMatchingUniques(UniqueType.RuinsUpgrade).map { it.params[0] }.firstOrNull()
if (unit.baseUnit().upgradesTo == null && specialUpgradesTo == null) return null // can't upgrade to anything
val unitTile = unit.getTile()
val civInfo = unit.civ
if (!isFree && unitTile.getOwner() != civInfo) return null
if (!isAnywhere && unitTile.getOwner() != civInfo) return null
val upgradesTo = unit.baseUnit().upgradesTo
val upgradedUnit = when {
@ -46,8 +47,9 @@ object UnitActionsUpgrade{
resourceRequirementsDelta.add(resource, -amount)
for ((resource, amount) in upgradedUnit.getResourceRequirementsPerTurn())
resourceRequirementsDelta.add(resource, amount)
for ((resource, _) in resourceRequirementsDelta.filter { it.value < 0 }) // filter copies, so no CCM
resourceRequirementsDelta[resource] = 0
val newResourceRequirementsString = resourceRequirementsDelta.entries
.filter { it.value > 0 }
.joinToString { "${it.value} {${it.key}}".tr() }
val goldCostOfUpgrade = if (isFree) 0 else unit.upgrade.getCostOfUpgrade(upgradedUnit)
@ -58,13 +60,19 @@ object UnitActionsUpgrade{
"Upgrade to [${upgradedUnit.name}] ([$goldCostOfUpgrade] gold)"
else "Upgrade to [${upgradedUnit.name}]\n([$goldCostOfUpgrade] gold, [$newResourceRequirementsString])"
return UnitAction(
UnitActionType.Upgrade,
return UpgradeUnitAction(
title = title,
unitToUpgradeTo = upgradedUnit,
goldCostOfUpgrade = goldCostOfUpgrade,
newResourceRequirements = resourceRequirementsDelta,
action = {
unit.destroy(destroyTransportedUnit = false)
val newUnit = civInfo.units.placeUnitNearTile(unitTile.position, upgradedUnit.name)
/** We were UNABLE to place the new unit, which means that the unit failed to upgrade!
* The only known cause of this currently is "land units upgrading to water units" which fail to be placed.
*/
/** We were UNABLE to place the new unit, which means that the unit failed to upgrade!
* The only known cause of this currently is "land units upgrading to water units" which fail to be placed.
*/
@ -80,6 +88,7 @@ object UnitActionsUpgrade{
isFree || (
unit.civ.gold >= goldCostOfUpgrade
&& unit.currentMovement > 0
&& unitTile.getOwner() == civInfo
&& !unit.isEmbarked()
&& unit.upgrade.canUpgrade(unitToUpgradeTo = upgradedUnit)
)
@ -88,10 +97,11 @@ object UnitActionsUpgrade{
}
fun getUpgradeAction(unit: MapUnit) =
getUpgradeAction(unit, isFree = false, isSpecial = false)
getUpgradeAction(unit, isFree = false, isSpecial = false, isAnywhere = false)
fun getFreeUpgradeAction(unit: MapUnit) =
getUpgradeAction(unit, isFree = true, isSpecial = false)
getUpgradeAction(unit, isFree = true, isSpecial = false, isAnywhere = true)
fun getAncientRuinsUpgradeAction(unit: MapUnit) =
getUpgradeAction(unit, isFree = true, isSpecial = true)
getUpgradeAction(unit, isFree = true, isSpecial = true, isAnywhere = true)
fun getUpgradeActionAnywhere(unit: MapUnit) =
getUpgradeAction(unit, isFree = false, isSpecial = false, isAnywhere = true)
}

View File

@ -96,6 +96,7 @@ These shapes are used all over Unciv and can be replaced to make a lot of UI ele
| TechPickerScreen/ | ResearchedFutureTechColor | 127, 50, 0 | |
| TechPickerScreen/ | ResearchedTechColor | 255, 215, 0 | |
| TechPickerScreen/ | TechButtonIconsOutline | roundedEdgeRectangleSmall | |
| UnitUpgradeMenu/ | Button | roundedEdgeRectangleMid | |
| VictoryScreen/ | CivGroup | roundedEdgeRectangle | |
| WorldScreen/ | AirUnitTable | null | |
| WorldScreen/ | BattleTable | null | |

View File

@ -294,3 +294,6 @@ List of attributes - note not all combinations are valid:
|`centered`|Boolean|Centers the line (and turns off automatic wrap).|
The lines from json will 'surround' the automatically generated lines such that the latter are inserted just above the first json line carrying a link, if any. If no json lines have links, they will be inserted between the automatic title and the automatic info. This method may, however, change in the future.
Note: `text` now also supports inline color markup. Insert `«color»` to start coloring text, `«»` to stop. `color` can be a name or 6/8-digit hex notation like `#ffa040` (different from the `color` attribute notation only by not allowing 3-digit codes, but allowing the alpha channel).
Effectively, the `«»` markers are replaced with `[]` _after_ translation and then passed to [Gdx markup language](https://libgdx.com/wiki/graphics/2d/fonts/color-markup-language).