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
21 changed files with 462 additions and 98 deletions

View File

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

View File

@ -3,6 +3,7 @@ package com.unciv.models
import com.badlogic.gdx.Input import com.badlogic.gdx.Input
import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.Actor
import com.unciv.Constants import com.unciv.Constants
import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.translations.getPlaceholderParameters import com.unciv.models.translations.getPlaceholderParameters
import com.unciv.ui.components.Fonts import com.unciv.ui.components.Fonts
import com.unciv.ui.components.KeyCharAndCode 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. /** Unit Actions - class - carries dynamic data and actual execution.
* Static properties are in [UnitActionType]. * 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 type: UnitActionType,
val title: String = type.value, val title: String = type.value,
val isCurrentAction: Boolean = false, val isCurrentAction: Boolean = false,
@ -38,8 +39,44 @@ data class UnitAction(
else -> ImageGetter.getUnitActionPortrait("Star") 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 /** Unit Actions - generic enum with static properties
* *
* @param value _default_ label to display, can be overridden in UnitAction instantiation * @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.components.extensions.colorFromRGB
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen.Companion.showReligionInCivilopedia import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen.Companion.showReligionInCivilopedia
import com.unciv.ui.screens.civilopediascreen.FormattedLine import com.unciv.ui.screens.civilopediascreen.FormattedLine
import com.unciv.ui.objectdescriptions.BaseUnitDescriptions
import kotlin.math.pow import kotlin.math.pow
class Nation : RulesetObject() { class Nation : RulesetObject() {
@ -230,42 +231,10 @@ class Nation : RulesetObject() {
yield(FormattedLine("Replaces [${originalUnit.name}]", link="Unit/${originalUnit.name}", indent=1)) yield(FormattedLine("Replaces [${originalUnit.name}]", link="Unit/${originalUnit.name}", indent=1))
if (unit.cost != originalUnit.cost) if (unit.cost != originalUnit.cost)
yield(FormattedLine("{Cost} ".tr() + "[${unit.cost}] vs [${originalUnit.cost}]".tr(), indent=1)) yield(FormattedLine("{Cost} ".tr() + "[${unit.cost}] vs [${originalUnit.cost}]".tr(), indent=1))
if (unit.strength != originalUnit.strength) yieldAll(
yield(FormattedLine("${Fonts.strength} " + "[${unit.strength}] vs [${originalUnit.strength}]".tr(), indent=1)) BaseUnitDescriptions.getDifferences(ruleset, originalUnit, unit)
if (unit.rangedStrength != originalUnit.rangedStrength) .map { (text, link) -> FormattedLine(text, link = link ?: "", indent = 1) }
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 )
)
}
} else if (unit.replaces != null) { } else if (unit.replaces != null) {
yield(FormattedLine("Replaces [${unit.replaces}], which is not found in the ruleset!", indent = 1)) yield(FormattedLine("Replaces [${unit.replaces}], which is not found in the ruleset!", indent = 1))
} else { } else {

View File

@ -12,7 +12,8 @@ import com.unciv.ui.screens.basescreen.BaseScreen
/** A Label allowing Gdx markup /** 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( class ColorMarkupLabel private constructor(
fontSize: Int, // inverted order so it can be differentiated from the translating 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. /** A Label allowing Gdx markup, auto-translated.
* *
* Since Gdx markup markers are interpreted and removed by translation, use «» instead. * 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) constructor(
: this(fontSize, mapMarkup(text)) 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 /** A Label automatically applying Gdx markup colors to symbols and rest of text separately -
* - _after_ translating [text]. * _**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, constructor(
textColor: Color, text: String,
symbolColor: Color = Color.WHITE, textColor: Color,
fontSize: Int = Constants.defaultFontSize) symbolColor: Color = Color.WHITE,
: this (fontSize, prepareText(text, textColor, symbolColor)) fontSize: Int = Constants.defaultFontSize
) : this (fontSize, prepareText(text, textColor, symbolColor))
/** Only if wrap was turned on, this is the prefWidth before. /** Only if wrap was turned on, this is the prefWidth before.
* Used for getMaxWidth as better estimate than the default 0. */ * 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 override fun getMaxWidth() = unwrappedPrefWidth // If unwrapped, we return 0 same as super
companion object { 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 val inverseColorMap = Colors.getColors().associate { it.value to it.key }
private fun Color.toMarkup(): String { private fun Color.toMarkup(): String {
val mapEntry = inverseColorMap[this] val mapEntry = inverseColorMap[this]
@ -102,9 +110,16 @@ class ColorMarkupLabel private constructor(
return "#" + toString().substring(0,6) 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 { private fun prepareText(text: String, textColor: Color, symbolColor: Color): String {
val translated = text.tr() val translated = text.tr()
if (textColor == Color.WHITE && symbolColor == Color.WHITE || translated.isBlank()) if ((textColor == Color.WHITE && symbolColor == Color.WHITE) || translated.isBlank())
return translated return translated
val tc = textColor.toMarkup() val tc = textColor.toMarkup()
if (textColor == symbolColor) if (textColor == symbolColor)

View File

@ -429,7 +429,7 @@ fun Group.addBorderAllowOpacity(size:Float, color: Color): Group {
/** get background Image for a new separator */ /** get background Image for a new separator */
private fun getSeparatorImage(color: Color) = ImageGetter.getDot( 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 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.logic.city.City
import com.unciv.models.ruleset.Ruleset 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.UniqueFlag
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit 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.getConsumesAmountString
import com.unciv.ui.components.extensions.toPercent import com.unciv.ui.components.extensions.toPercent
import com.unciv.ui.screens.civilopediascreen.FormattedLine 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 import kotlin.math.pow
object BaseUnitDescriptions { object BaseUnitDescriptions {
@ -196,6 +204,7 @@ object BaseUnitDescriptions {
return textList return textList
} }
@Suppress("RemoveExplicitTypeArguments") // for faster IDE - inferring sequence types can be slow
fun UnitType.getUnitTypeCivilopediaTextLines(ruleset: Ruleset): List<FormattedLine> { fun UnitType.getUnitTypeCivilopediaTextLines(ruleset: Ruleset): List<FormattedLine> {
fun getDomainLines() = sequence<FormattedLine> { fun getDomainLines() = sequence<FormattedLine> {
yield(FormattedLine("{Unit types}:", header = 4)) yield(FormattedLine("{Unit types}:", header = 4))
@ -231,4 +240,84 @@ object BaseUnitDescriptions {
} }
return (if (name.startsWith("Domain: ")) getDomainLines() else getUnitTypeLines()).toList() 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("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")) 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() { fun update() {
clear() clear()
val colorSelected = BaseScreen.skin.get("selection", Color::class.java) val colorSelected = BaseScreen.skin.getColor("selection")
val colorButton = BaseScreen.skin.get("color", Color::class.java) val colorButton = BaseScreen.skin.getColor("color")
// effectively a button, but didn't want to rewrite TextButton style // effectively a button, but didn't want to rewrite TextButton style
// and much more compact and can control backgrounds easily based on settings // and much more compact and can control backgrounds easily based on settings
val resetLabel = "Reset Citizens".toLabel() val resetLabel = "Reset Citizens".toLabel()

View File

@ -77,7 +77,7 @@ class CityStatsTable(private val cityScreen: CityScreen): Table() {
lowerTable.clear() lowerTable.clear()
val miniStatsTable = Table() 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) { for ((stat, amount) in cityInfo.cityStats.currentCityStats) {
if (stat == Stat.Faith && !cityInfo.civ.gameInfo.isReligionEnabled()) continue if (stat == Stat.Faith && !cityInfo.civ.gameInfo.isReligionEnabled()) continue
val icon = Table() val icon = Table()

View File

@ -27,7 +27,7 @@ class SpecialistAllocationTable(private val cityScreen: CityScreen) : Table(Base
if (cityScreen.canCityBeChanged()) { if (cityScreen.canCityBeChanged()) {
if (cityInfo.manualSpecialists) { if (cityInfo.manualSpecialists) {
val manualSpecialists = "Manual Specialists".toLabel() val manualSpecialists = "Manual Specialists".toLabel()
.addBorder(5f, BaseScreen.skin.get("color", Color::class.java)) .addBorder(5f, BaseScreen.skin.getColor("color"))
manualSpecialists.onClick { manualSpecialists.onClick {
cityInfo.manualSpecialists = false cityInfo.manualSpecialists = false
cityInfo.reassignPopulation(); cityScreen.update() cityInfo.reassignPopulation(); cityScreen.update()
@ -35,7 +35,7 @@ class SpecialistAllocationTable(private val cityScreen: CityScreen) : Table(Base
add(manualSpecialists).colspan(5).row() add(manualSpecialists).colspan(5).row()
} else { } else {
val autoSpecialists = "Auto Specialists".toLabel() 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() } autoSpecialists.onClick { cityInfo.manualSpecialists = true; update() }
add(autoSpecialists).colspan(5).row() 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.Ruleset
import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.Unique
import com.unciv.ui.components.ColorMarkupLabel
import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.components.extensions.toLabel
import com.unciv.utils.Log import com.unciv.utils.Log
import kotlin.math.max import kotlin.math.max
@ -258,7 +260,7 @@ class FormattedLine (
size == Int.MIN_VALUE -> Constants.defaultFontSize size == Int.MIN_VALUE -> Constants.defaultFontSize
else -> size else -> size
} }
val labelColor = if(starred) defaultColor else displayColor val labelColor = if (starred) defaultColor else displayColor
val table = Table(BaseScreen.skin) val table = Table(BaseScreen.skin)
var iconCount = 0 var iconCount = 0
@ -285,7 +287,10 @@ class FormattedLine (
else -> (indent-1) * indentPad + else -> (indent-1) * indentPad +
indentOneAtNumIcons * (minIconSize + iconPad) + iconPad - usedWidth 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.wrap = !centered && labelWidth > 0f
label.setAlignment(align) label.setAlignment(align)
if (labelWidth == 0f) if (labelWidth == 0f)

View File

@ -28,7 +28,7 @@ object MarkupRenderer {
* @param linkAction Delegate to call for internal links. Leave null to suppress linking. * @param linkAction Delegate to call for internal links. Leave null to suppress linking.
*/ */
fun render( fun render(
lines: Collection<FormattedLine>, lines: Iterable<FormattedLine>,
labelWidth: Float = 0f, labelWidth: Float = 0f,
padding: Float = defaultPadding, padding: Float = defaultPadding,
iconDisplay: FormattedLine.IconDisplay = FormattedLine.IconDisplay.All, 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" add(arrowWrapper).align(Align.center).width(handleWidth).fillY().apply { // the "handle"
background = BaseScreen.skinStrings.getUiBackground( background = BaseScreen.skinStrings.getUiBackground(
"MapEditor/MapEditorToolsDrawer/Handle", "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 } .filter { it.type.isBaseTerrain }
private fun getTerrains() = allTerrains() private fun getTerrains() = allTerrains()
.map { FormattedLine(it.name, it.name, "Terrain/${it.name}", size = 32) } .map { FormattedLine(it.name, it.name, "Terrain/${it.name}", size = 32) }
.toList() .asIterable()
override fun isDisabled() = false // allTerrains().none() // wanna see _that_ mod... override fun isDisabled() = false // allTerrains().none() // wanna see _that_ mod...
} }
@ -100,7 +100,7 @@ class MapEditorEditFeaturesTab(
.filter { it.type == TerrainType.TerrainFeature } .filter { it.type == TerrainType.TerrainFeature }
private fun getFeatures() = allowedFeatures() private fun getFeatures() = allowedFeatures()
.map { FormattedLine(it.name, it.name, "Terrain/${it.name}", size = 32) } .map { FormattedLine(it.name, it.name, "Terrain/${it.name}", size = 32) }
.toList() .asIterable()
override fun isDisabled() = allowedFeatures().none() override fun isDisabled() = allowedFeatures().none()
} }
@ -133,7 +133,7 @@ class MapEditorEditWondersTab(
.filter { it.type == TerrainType.NaturalWonder } .filter { it.type == TerrainType.NaturalWonder }
private fun getWonders() = allowedWonders() private fun getWonders() = allowedWonders()
.map { FormattedLine(it.name, it.name, "Terrain/${it.name}", size = 32) } .map { FormattedLine(it.name, it.name, "Terrain/${it.name}", size = 32) }
.toList() .asIterable()
override fun isDisabled() = allowedWonders().none() override fun isDisabled() = allowedWonders().none()
} }
@ -176,7 +176,7 @@ class MapEditorEditResourcesTab(
private fun allowedResources() = ruleset.tileResources.values.asSequence() private fun allowedResources() = ruleset.tileResources.values.asSequence()
.filter { !it.hasUnique(UniqueType.CityStateOnlyResource) } .filter { !it.hasUnique(UniqueType.CityStateOnlyResource) }
private fun getResources(): List<FormattedLine> = sequence { private fun getResources(): Iterable<FormattedLine> = sequence {
var lastGroup = ResourceType.Bonus var lastGroup = ResourceType.Bonus
for (resource in allowedResources()) { for (resource in allowedResources()) {
val name = resource.name val name = resource.name
@ -186,7 +186,7 @@ class MapEditorEditResourcesTab(
} }
yield (FormattedLine(name, name, "Resource/$name", size = 32)) yield (FormattedLine(name, name, "Resource/$name", size = 32))
} }
}.toList() }.asIterable()
override fun isDisabled() = allowedResources().none() override fun isDisabled() = allowedResources().none()
} }
@ -233,7 +233,7 @@ class MapEditorEditImprovementsTab(
.filter { improvement -> .filter { improvement ->
disallowImprovements.none { improvement.name.startsWith(it) } disallowImprovements.none { improvement.name.startsWith(it) }
} }
private fun getImprovements(): List<FormattedLine> = sequence { private fun getImprovements(): Iterable<FormattedLine> = sequence {
var lastGroup = 0 var lastGroup = 0
for (improvement in allowedImprovements()) { for (improvement in allowedImprovements()) {
val name = improvement.name val name = improvement.name
@ -244,7 +244,7 @@ class MapEditorEditImprovementsTab(
} }
yield (FormattedLine(name, name, "Improvement/$name", size = 32)) yield (FormattedLine(name, name, "Improvement/$name", size = 32))
} }
}.toList() }.asIterable()
override fun isDisabled() = allowedImprovements().none() override fun isDisabled() = allowedImprovements().none()
@ -306,7 +306,7 @@ class MapEditorEditStartsTab(
private fun getNations() = allowedNations() private fun getNations() = allowedNations()
.sortedWith(compareBy<Nation>{ it.isCityState }.thenBy(collator) { it.name.tr() }) .sortedWith(compareBy<Nation>{ it.isCityState }.thenBy(collator) { it.name.tr() })
.map { FormattedLine("[${it.name}] starting location", it.name, "Nation/${it.name}", size = 24) } .map { FormattedLine("[${it.name}] starting location", it.name, "Nation/${it.name}", size = 24) }
.toList() .asIterable()
override fun isDisabled() = allowedNations().none() override fun isDisabled() = allowedNations().none()

View File

@ -138,7 +138,7 @@ class MapEditorViewTab(
startsOutOpened = false, startsOutOpened = false,
headerPad = 5f headerPad = 5f
) { ) {
it.add(MarkupRenderer.render(lines.toList(), iconDisplay = IconDisplay.NoLink) { name -> it.add(MarkupRenderer.render(lines.asIterable(), iconDisplay = IconDisplay.NoLink) { name ->
scrollToStartOfNation(name) scrollToStartOfNation(name)
}) })
}).row() }).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.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile import com.unciv.logic.map.tile.Tile
import com.unciv.models.UnitActionType 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.ExpanderTab
import com.unciv.ui.components.Fonts import com.unciv.ui.components.Fonts
import com.unciv.ui.components.TabbedPager import com.unciv.ui.components.TabbedPager
@ -273,16 +274,20 @@ class UnitOverviewTab(
add(promotionsTable) add(promotionsTable)
// Upgrade column // Upgrade column
if (unit.upgrade.canUpgrade()) { val unitAction = UnitActionsUpgrade.getUpgradeActionAnywhere(unit)
val unitAction = UnitActionsUpgrade.getUpgradeAction(unit) if (unitAction != null) {
val enable = unitAction?.action != null && viewingPlayer.isCurrentPlayer() && val enable = unitAction.action != null && viewingPlayer.isCurrentPlayer() &&
GUI.isAllowedChangeState() 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) Color.GREEN else Color.GREEN.darken(0.5f))
if (enable) upgradeIcon.onClick { if (enable) upgradeIcon.onClick {
SoundPlayer.play(unitAction!!.uncivSound) val pos = upgradeIcon.localToStageCoordinates(Vector2(upgradeIcon.width/2, upgradeIcon.height/2))
unitAction.action!!() UnitUpgradeMenu(overviewScreen.stage, pos, unit, unitAction) {
unitListTable.updateUnitListTable() unitListTable.updateUnitListTable()
select(selectKey)
}
} }
add(upgradeIcon).size(28f) add(upgradeIcon).size(28f)
} else add() } else add()
@ -295,7 +300,10 @@ class UnitOverviewTab(
} }
companion object { 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? { override fun select(selection: String): Float? {
@ -316,4 +324,5 @@ class UnitOverviewTab(
button.addAction(blinkAction) button.addAction(blinkAction)
return scrollY 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 package com.unciv.ui.screens.worldscreen.unit.actions
import com.badlogic.gdx.graphics.Color 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.Button
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.unciv.GUI import com.unciv.GUI
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.models.UnitAction import com.unciv.models.UnitAction
import com.unciv.models.UnitActionType import com.unciv.models.UnitActionType
import com.unciv.models.UpgradeUnitAction
import com.unciv.ui.components.KeyCharAndCode 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.UncivTooltip.Companion.addTooltip
import com.unciv.ui.components.extensions.disable import com.unciv.ui.components.extensions.disable
import com.unciv.ui.components.extensions.keyShortcuts import com.unciv.ui.components.extensions.keyShortcuts
import com.unciv.ui.components.extensions.onActivation import com.unciv.ui.components.extensions.onActivation
import com.unciv.ui.components.extensions.packIfNeeded
import com.unciv.ui.images.IconTextButton import com.unciv.ui.images.IconTextButton
import com.unciv.ui.objectdescriptions.BaseUnitDescriptions
import com.unciv.ui.screens.worldscreen.WorldScreen import com.unciv.ui.screens.worldscreen.WorldScreen
class UnitActionsTable(val worldScreen: WorldScreen) : Table() { class UnitActionsTable(val worldScreen: WorldScreen) : Table() {
@ -22,9 +28,17 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() {
clear() clear()
if (unit == null) return if (unit == null) return
if (!worldScreen.canChangeState) return // No actions when it's not your turn or spectator! if (!worldScreen.canChangeState) return // No actions when it's not your turn or spectator!
for (button in UnitActions.getUnitActions(unit) for (unitAction in UnitActions.getUnitActions(unit)) {
.map { getUnitActionButton(unit, it) }) 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() add(button).left().padBottom(2f).row()
}
pack() pack()
} }
@ -40,7 +54,8 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() {
if (unitAction.type == UnitActionType.Promote && unitAction.action != null) if (unitAction.type == UnitActionType.Promote && unitAction.action != null)
actionButton.color = Color.GREEN.cpy().lerp(Color.WHITE, 0.5f) 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() actionButton.pack()
if (unitAction.action == null) { if (unitAction.action == null) {
actionButton.disable() 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.logic.map.mapunit.MapUnit
import com.unciv.models.Counter import com.unciv.models.Counter
import com.unciv.models.UnitAction 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.ruleset.unique.UniqueType
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
object UnitActionsUpgrade{ object UnitActionsUpgrade {
internal fun addUnitUpgradeAction( internal fun addUnitUpgradeAction(
unit: MapUnit, unit: MapUnit,
@ -21,13 +21,14 @@ object UnitActionsUpgrade{
private fun getUpgradeAction( private fun getUpgradeAction(
unit: MapUnit, unit: MapUnit,
isFree: Boolean, isFree: Boolean,
isSpecial: Boolean isSpecial: Boolean,
isAnywhere: Boolean
): UnitAction? { ): UnitAction? {
val specialUpgradesTo = unit.baseUnit().getMatchingUniques(UniqueType.RuinsUpgrade).map { it.params[0] }.firstOrNull() 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 if (unit.baseUnit().upgradesTo == null && specialUpgradesTo == null) return null // can't upgrade to anything
val unitTile = unit.getTile() val unitTile = unit.getTile()
val civInfo = unit.civ val civInfo = unit.civ
if (!isFree && unitTile.getOwner() != civInfo) return null if (!isAnywhere && unitTile.getOwner() != civInfo) return null
val upgradesTo = unit.baseUnit().upgradesTo val upgradesTo = unit.baseUnit().upgradesTo
val upgradedUnit = when { val upgradedUnit = when {
@ -46,8 +47,9 @@ object UnitActionsUpgrade{
resourceRequirementsDelta.add(resource, -amount) resourceRequirementsDelta.add(resource, -amount)
for ((resource, amount) in upgradedUnit.getResourceRequirementsPerTurn()) for ((resource, amount) in upgradedUnit.getResourceRequirementsPerTurn())
resourceRequirementsDelta.add(resource, amount) resourceRequirementsDelta.add(resource, amount)
for ((resource, _) in resourceRequirementsDelta.filter { it.value < 0 }) // filter copies, so no CCM
resourceRequirementsDelta[resource] = 0
val newResourceRequirementsString = resourceRequirementsDelta.entries val newResourceRequirementsString = resourceRequirementsDelta.entries
.filter { it.value > 0 }
.joinToString { "${it.value} {${it.key}}".tr() } .joinToString { "${it.value} {${it.key}}".tr() }
val goldCostOfUpgrade = if (isFree) 0 else unit.upgrade.getCostOfUpgrade(upgradedUnit) val goldCostOfUpgrade = if (isFree) 0 else unit.upgrade.getCostOfUpgrade(upgradedUnit)
@ -58,13 +60,19 @@ object UnitActionsUpgrade{
"Upgrade to [${upgradedUnit.name}] ([$goldCostOfUpgrade] gold)" "Upgrade to [${upgradedUnit.name}] ([$goldCostOfUpgrade] gold)"
else "Upgrade to [${upgradedUnit.name}]\n([$goldCostOfUpgrade] gold, [$newResourceRequirementsString])" else "Upgrade to [${upgradedUnit.name}]\n([$goldCostOfUpgrade] gold, [$newResourceRequirementsString])"
return UnitAction( return UpgradeUnitAction(
UnitActionType.Upgrade,
title = title, title = title,
unitToUpgradeTo = upgradedUnit,
goldCostOfUpgrade = goldCostOfUpgrade,
newResourceRequirements = resourceRequirementsDelta,
action = { action = {
unit.destroy(destroyTransportedUnit = false) unit.destroy(destroyTransportedUnit = false)
val newUnit = civInfo.units.placeUnitNearTile(unitTile.position, upgradedUnit.name) 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! /** 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. * 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 || ( isFree || (
unit.civ.gold >= goldCostOfUpgrade unit.civ.gold >= goldCostOfUpgrade
&& unit.currentMovement > 0 && unit.currentMovement > 0
&& unitTile.getOwner() == civInfo
&& !unit.isEmbarked() && !unit.isEmbarked()
&& unit.upgrade.canUpgrade(unitToUpgradeTo = upgradedUnit) && unit.upgrade.canUpgrade(unitToUpgradeTo = upgradedUnit)
) )
@ -88,10 +97,11 @@ object UnitActionsUpgrade{
} }
fun getUpgradeAction(unit: MapUnit) = fun getUpgradeAction(unit: MapUnit) =
getUpgradeAction(unit, isFree = false, isSpecial = false) getUpgradeAction(unit, isFree = false, isSpecial = false, isAnywhere = false)
fun getFreeUpgradeAction(unit: MapUnit) = fun getFreeUpgradeAction(unit: MapUnit) =
getUpgradeAction(unit, isFree = true, isSpecial = false) getUpgradeAction(unit, isFree = true, isSpecial = false, isAnywhere = true)
fun getAncientRuinsUpgradeAction(unit: MapUnit) = 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/ | ResearchedFutureTechColor | 127, 50, 0 | |
| TechPickerScreen/ | ResearchedTechColor | 255, 215, 0 | | | TechPickerScreen/ | ResearchedTechColor | 255, 215, 0 | |
| TechPickerScreen/ | TechButtonIconsOutline | roundedEdgeRectangleSmall | | | TechPickerScreen/ | TechButtonIconsOutline | roundedEdgeRectangleSmall | |
| UnitUpgradeMenu/ | Button | roundedEdgeRectangleMid | |
| VictoryScreen/ | CivGroup | roundedEdgeRectangle | | | VictoryScreen/ | CivGroup | roundedEdgeRectangle | |
| WorldScreen/ | AirUnitTable | null | | | WorldScreen/ | AirUnitTable | null | |
| WorldScreen/ | BattleTable | 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).| |`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. 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).