mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-30 15:30:43 -04:00
Promotion picker allowing picking chains in one go (#9655)
* Try to allow chained promotion picking * Reorg PromotionPickerScreen into own package * Draft for new PromotionTree * Change PromotionPickerScreen to use new tree (picking still not done) * Finish new PromotionPickerScreen - code * Finish new PromotionPickerScreen - assets and linting * Finish new PromotionPickerScreen - polish positioning and lines * Finish new PromotionPickerScreen - fix sound * Finish new PromotionPickerScreen - little optimization * Finish new PromotionPickerScreen - emphasize line along path * Finish new PromotionPickerScreen - merge fix * Finish new PromotionPickerScreen - address comments * Finish new PromotionPickerScreen - fix sort and update wiki
This commit is contained in:
parent
d298f85099
commit
c45d3ecb7c
@ -82,6 +82,60 @@
|
|||||||
"g": 0.0627451,
|
"g": 0.0627451,
|
||||||
"b": 0.039215688,
|
"b": 0.039215688,
|
||||||
"a": 1
|
"a": 1
|
||||||
|
},
|
||||||
|
"promotion-default": {
|
||||||
|
"r": 0,
|
||||||
|
"g": 0,
|
||||||
|
"b": 0,
|
||||||
|
"a": 1
|
||||||
|
},
|
||||||
|
"promotion-selected": {
|
||||||
|
"r": 0.2824,
|
||||||
|
"g": 0.5765,
|
||||||
|
"b": 0.6863,
|
||||||
|
"a": 1
|
||||||
|
},
|
||||||
|
"promotion-path": {
|
||||||
|
"r": 0.1882,
|
||||||
|
"g": 0.3843,
|
||||||
|
"b": 0.4575,
|
||||||
|
"a": 1
|
||||||
|
},
|
||||||
|
"promotion-promoted": {
|
||||||
|
"r": 0.8,
|
||||||
|
"g": 0.6745,
|
||||||
|
"b": 0,
|
||||||
|
"a": 1
|
||||||
|
},
|
||||||
|
"promotion-promoted-text": {
|
||||||
|
"r": 0.16,
|
||||||
|
"g": 0.1349,
|
||||||
|
"b": 0,
|
||||||
|
"a": 1
|
||||||
|
},
|
||||||
|
"promotion-pickable": {
|
||||||
|
"r": 0.1098,
|
||||||
|
"g": 0.3137,
|
||||||
|
"b": 0,
|
||||||
|
"a": 1
|
||||||
|
},
|
||||||
|
"promotion-prerequisite": {
|
||||||
|
"r": 0.405,
|
||||||
|
"g": 0.506,
|
||||||
|
"b": 0.81,
|
||||||
|
"a": 1
|
||||||
|
},
|
||||||
|
"promotion-grouplines": {
|
||||||
|
"r": 1,
|
||||||
|
"g": 1,
|
||||||
|
"b": 1,
|
||||||
|
"a": 1
|
||||||
|
},
|
||||||
|
"promotion-otherlines": {
|
||||||
|
"r": 1,
|
||||||
|
"g": 0.7,
|
||||||
|
"b": 0,
|
||||||
|
"a": 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"com.badlogic.gdx.scenes.scene2d.ui.Skin$TintedDrawable": {
|
"com.badlogic.gdx.scenes.scene2d.ui.Skin$TintedDrawable": {
|
||||||
@ -319,5 +373,18 @@
|
|||||||
"cursor": "white",
|
"cursor": "white",
|
||||||
"selection": "selection"
|
"selection": "selection"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"com.unciv.ui.screens.pickerscreens.PromotionScreenColors": {
|
||||||
|
"default": {
|
||||||
|
"default": "promotion-default",
|
||||||
|
"selected": "promotion-selected",
|
||||||
|
"pathToSelection": "promotion-path",
|
||||||
|
"promoted": "promotion-promoted",
|
||||||
|
"promotedText": "promotion-promoted-text",
|
||||||
|
"pickable": "promotion-pickable",
|
||||||
|
"prerequisite": "promotion-prerequisite",
|
||||||
|
"groupLines": "promotion-grouplines",
|
||||||
|
"otherLines": "promotion-otherlines"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1685,6 +1685,7 @@ Dogfighting III = Kurvenkampf III
|
|||||||
Choose name for [unitName] = Wähle Namen für [unitName]
|
Choose name for [unitName] = Wähle Namen für [unitName]
|
||||||
[unitFilter] units gain the [promotion] promotion = [unitFilter] Einheiten erhalten die [promotion] Beförderung
|
[unitFilter] units gain the [promotion] promotion = [unitFilter] Einheiten erhalten die [promotion] Beförderung
|
||||||
Requires = Benötigt
|
Requires = Benötigt
|
||||||
|
Path to [promotion] is ambiguous = Der Weg zu [promotion] ist noch nicht klar
|
||||||
|
|
||||||
# Multiplayer Turn Checker Service
|
# Multiplayer Turn Checker Service
|
||||||
|
|
||||||
|
@ -1684,6 +1684,7 @@ Dogfighting III =
|
|||||||
Choose name for [unitName] =
|
Choose name for [unitName] =
|
||||||
[unitFilter] units gain the [promotion] promotion =
|
[unitFilter] units gain the [promotion] promotion =
|
||||||
Requires =
|
Requires =
|
||||||
|
Path to [promotion] is ambiguous =
|
||||||
|
|
||||||
# Multiplayer Turn Checker Service
|
# Multiplayer Turn Checker Service
|
||||||
|
|
||||||
|
@ -51,6 +51,9 @@ class UnitPromotions : IsPartOfGameInfoSerialization {
|
|||||||
/** @return the XP points needed to "buy" the next promotion. 10, 30, 60, 100, 150,... */
|
/** @return the XP points needed to "buy" the next promotion. 10, 30, 60, 100, 150,... */
|
||||||
fun xpForNextPromotion() = (numberOfPromotions + 1) * 10
|
fun xpForNextPromotion() = (numberOfPromotions + 1) * 10
|
||||||
|
|
||||||
|
/** @return the XP points needed to "buy" the next [count] promotions. */
|
||||||
|
fun xpForNextNPromotions(count: Int) = (1..count).sumOf { (numberOfPromotions + it) * 10 }
|
||||||
|
|
||||||
/** @return Total XP including that already "spent" on promotions */
|
/** @return Total XP including that already "spent" on promotions */
|
||||||
fun totalXpProduced() = XP + (numberOfPromotions * (numberOfPromotions + 1)) * 5
|
fun totalXpProduced() = XP + (numberOfPromotions * (numberOfPromotions + 1)) * 5
|
||||||
|
|
||||||
|
@ -162,7 +162,7 @@ class Ruleset {
|
|||||||
cityStateTypes.putAll(ruleset.cityStateTypes)
|
cityStateTypes.putAll(ruleset.cityStateTypes)
|
||||||
ruleset.modOptions.unitsToRemove
|
ruleset.modOptions.unitsToRemove
|
||||||
.flatMap { unitToRemove ->
|
.flatMap { unitToRemove ->
|
||||||
units.filter { it.apply { value.ruleset=this@Ruleset }.value.matchesFilter(unitToRemove) }.keys
|
units.filter { it.apply { value.ruleset = this@Ruleset }.value.matchesFilter(unitToRemove) }.keys
|
||||||
}.toSet().forEach {
|
}.toSet().forEach {
|
||||||
units.remove(it)
|
units.remove(it)
|
||||||
}
|
}
|
||||||
@ -170,30 +170,7 @@ class Ruleset {
|
|||||||
modOptions.uniques.addAll(ruleset.modOptions.uniques)
|
modOptions.uniques.addAll(ruleset.modOptions.uniques)
|
||||||
modOptions.constants.merge(ruleset.modOptions.constants)
|
modOptions.constants.merge(ruleset.modOptions.constants)
|
||||||
|
|
||||||
// Allow each mod to define their own columns, and if there's a conflict, later mods will be shifted right
|
unitPromotions.putAll(ruleset.unitPromotions)
|
||||||
// We should never be editing the original ruleset objects, only copies
|
|
||||||
val addRulesetUnitPromotionClones = ruleset.unitPromotions.values.map { it.clone() }
|
|
||||||
val existingPromotionLocations =
|
|
||||||
unitPromotions.values.map { "${it.row}/${it.column}" }.toHashSet()
|
|
||||||
val promotionsWithConflictingLocations = addRulesetUnitPromotionClones.filter {
|
|
||||||
existingPromotionLocations.contains("${it.row}/${it.column}")
|
|
||||||
}
|
|
||||||
val columnsWithConflictingLocations =
|
|
||||||
promotionsWithConflictingLocations.map { it.column }.distinct()
|
|
||||||
|
|
||||||
if (columnsWithConflictingLocations.isNotEmpty()) {
|
|
||||||
var highestExistingColumn = unitPromotions.values.maxOf { it.column }
|
|
||||||
for (conflictingColumn in columnsWithConflictingLocations) {
|
|
||||||
highestExistingColumn += 1
|
|
||||||
val newColumn = highestExistingColumn
|
|
||||||
for (promotion in addRulesetUnitPromotionClones)
|
|
||||||
if (promotion.column == conflictingColumn)
|
|
||||||
promotion.column = newColumn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val finalModUnitPromotionsMap = addRulesetUnitPromotionClones.associateBy { it.name }
|
|
||||||
|
|
||||||
unitPromotions.putAll(finalModUnitPromotionsMap)
|
|
||||||
|
|
||||||
mods += ruleset.mods
|
mods += ruleset.mods
|
||||||
}
|
}
|
||||||
@ -312,14 +289,6 @@ class Ruleset {
|
|||||||
val promotionsFile = folderHandle.child("UnitPromotions.json")
|
val promotionsFile = folderHandle.child("UnitPromotions.json")
|
||||||
if (promotionsFile.exists()) unitPromotions += createHashmap(json().fromJsonFile(Array<Promotion>::class.java, promotionsFile))
|
if (promotionsFile.exists()) unitPromotions += createHashmap(json().fromJsonFile(Array<Promotion>::class.java, promotionsFile))
|
||||||
|
|
||||||
var topRow = unitPromotions.values.filter { it.column == 0 }.maxOfOrNull { it.row } ?: -1
|
|
||||||
for (promotion in unitPromotions.values)
|
|
||||||
if (promotion.row == -1){
|
|
||||||
promotion.column = 0
|
|
||||||
topRow += 1
|
|
||||||
promotion.row = topRow
|
|
||||||
}
|
|
||||||
|
|
||||||
val questsFile = folderHandle.child("Quests.json")
|
val questsFile = folderHandle.child("Quests.json")
|
||||||
if (questsFile.exists()) quests += createHashmap(json().fromJsonFile(Array<Quest>::class.java, questsFile))
|
if (questsFile.exists()) quests += createHashmap(json().fromJsonFile(Array<Quest>::class.java, questsFile))
|
||||||
|
|
||||||
|
@ -7,18 +7,23 @@ import com.unciv.models.ruleset.unique.UniqueTarget
|
|||||||
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
|
||||||
import com.unciv.ui.screens.civilopediascreen.FormattedLine
|
import com.unciv.ui.screens.civilopediascreen.FormattedLine
|
||||||
|
import com.unciv.ui.screens.pickerscreens.PromotionPickerScreen
|
||||||
|
|
||||||
class Promotion : RulesetObject() {
|
class Promotion : RulesetObject() {
|
||||||
var prerequisites = listOf<String>()
|
var prerequisites = listOf<String>()
|
||||||
|
|
||||||
var unitTypes = listOf<String>() // The json parser wouldn't agree to deserialize this as a list of UnitTypes. =(
|
var unitTypes = listOf<String>() // The json parser wouldn't agree to deserialize this as a list of UnitTypes. =(
|
||||||
|
|
||||||
/** Row of -1 determines that the modder has not set a position */
|
/** Used as **column** hint in the current [PromotionPickerScreen]
|
||||||
|
* This is no longer a direct position, it is used to sort before an automatic distribution.
|
||||||
|
* -1 determines that the modder has not set a position */
|
||||||
var row = -1
|
var row = -1
|
||||||
|
/** Used as **row** hint in the current [PromotionPickerScreen]
|
||||||
|
* This is no longer a direct position, it is used to sort before an automatic distribution.
|
||||||
|
*/
|
||||||
var column = 0
|
var column = 0
|
||||||
|
|
||||||
fun clone():Promotion {
|
fun clone(): Promotion {
|
||||||
val newPromotion = Promotion()
|
val newPromotion = Promotion()
|
||||||
|
|
||||||
// RulesetObject fields
|
// RulesetObject fields
|
||||||
@ -36,7 +41,7 @@ class Promotion : RulesetObject() {
|
|||||||
override fun getUniqueTarget() = UniqueTarget.Promotion
|
override fun getUniqueTarget() = UniqueTarget.Promotion
|
||||||
|
|
||||||
|
|
||||||
/** Used to describe a Promotion on the PromotionPickerScreen */
|
/** Used to describe a Promotion on the PromotionPickerScreen - fully translated */
|
||||||
fun getDescription(promotionsForUnitType: Collection<Promotion>):String {
|
fun getDescription(promotionsForUnitType: Collection<Promotion>):String {
|
||||||
val textList = ArrayList<String>()
|
val textList = ArrayList<String>()
|
||||||
|
|
||||||
@ -146,4 +151,27 @@ class Promotion : RulesetObject() {
|
|||||||
|
|
||||||
return textList
|
return textList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
data class PromotionBaseNameAndLevel(
|
||||||
|
val nameWithoutBrackets: String,
|
||||||
|
val level: Int,
|
||||||
|
val basePromotionName: String
|
||||||
|
)
|
||||||
|
/** Split a promotion name into base and level, e.g. "Drill II" -> 2 to "Drill"
|
||||||
|
*
|
||||||
|
* Used by Portrait (where it only has the string, the Promotion object is forgotten) and
|
||||||
|
* PromotionPickerScreen. Here to allow clear "Promotion.getBaseNameAndLevel" signature.
|
||||||
|
*/
|
||||||
|
fun getBaseNameAndLevel(promotionName: String): PromotionBaseNameAndLevel {
|
||||||
|
val nameWithoutBrackets = promotionName.replace("[", "").replace("]", "")
|
||||||
|
val level = when {
|
||||||
|
nameWithoutBrackets.endsWith(" I") -> 1
|
||||||
|
nameWithoutBrackets.endsWith(" II") -> 2
|
||||||
|
nameWithoutBrackets.endsWith(" III") -> 3
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
return PromotionBaseNameAndLevel(nameWithoutBrackets, level, nameWithoutBrackets.dropLast(if (level == 0) 0 else level + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Image
|
|||||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||||
import com.badlogic.gdx.utils.Align
|
import com.badlogic.gdx.utils.Align
|
||||||
import com.unciv.models.ruleset.Ruleset
|
import com.unciv.models.ruleset.Ruleset
|
||||||
|
import com.unciv.models.ruleset.unit.Promotion
|
||||||
import com.unciv.models.stats.Stats
|
import com.unciv.models.stats.Stats
|
||||||
import com.unciv.ui.components.extensions.center
|
import com.unciv.ui.components.extensions.center
|
||||||
import com.unciv.ui.components.extensions.centerX
|
import com.unciv.ui.components.extensions.centerX
|
||||||
@ -272,25 +273,16 @@ class PortraitPromotion(name: String, size: Float) : Portrait(Type.Promotion, na
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getDefaultImage(): Image {
|
override fun getDefaultImage(): Image {
|
||||||
|
val (nameWithoutBrackets, level, basePromotionName) = Promotion.getBaseNameAndLevel(imageName)
|
||||||
|
|
||||||
val nameWithoutBrackets = imageName.replace("[", "").replace("]", "")
|
this.level = level
|
||||||
|
|
||||||
level = when {
|
|
||||||
nameWithoutBrackets.endsWith(" I") -> 1
|
|
||||||
nameWithoutBrackets.endsWith(" II") -> 2
|
|
||||||
nameWithoutBrackets.endsWith(" III") -> 3
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
|
|
||||||
val basePromotionName = nameWithoutBrackets.dropLast(if (level == 0) 0 else level + 1)
|
|
||||||
|
|
||||||
val pathWithoutBrackets = "UnitPromotionIcons/$nameWithoutBrackets"
|
val pathWithoutBrackets = "UnitPromotionIcons/$nameWithoutBrackets"
|
||||||
val pathBase = "UnitPromotionIcons/$basePromotionName"
|
val pathBase = "UnitPromotionIcons/$basePromotionName"
|
||||||
val pathUnit = "UnitIcons/${basePromotionName.removeSuffix(" ability")}"
|
val pathUnit = "UnitIcons/${basePromotionName.removeSuffix(" ability")}"
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
ImageGetter.imageExists(pathWithoutBrackets) -> {
|
ImageGetter.imageExists(pathWithoutBrackets) -> {
|
||||||
level = 0
|
this.level = 0
|
||||||
ImageGetter.getImage(pathWithoutBrackets)
|
ImageGetter.getImage(pathWithoutBrackets)
|
||||||
}
|
}
|
||||||
ImageGetter.imageExists(pathBase) -> ImageGetter.getImage(pathBase)
|
ImageGetter.imageExists(pathBase) -> ImageGetter.getImage(pathBase)
|
||||||
|
@ -6,6 +6,8 @@ import com.badlogic.gdx.scenes.scene2d.Action
|
|||||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||||
import com.badlogic.gdx.scenes.scene2d.Group
|
import com.badlogic.gdx.scenes.scene2d.Group
|
||||||
import com.badlogic.gdx.scenes.scene2d.actions.Actions
|
import com.badlogic.gdx.scenes.scene2d.actions.Actions
|
||||||
|
import com.badlogic.gdx.scenes.scene2d.ui.Cell
|
||||||
|
import com.badlogic.gdx.scenes.scene2d.ui.Image
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||||
import com.badlogic.gdx.utils.Align
|
import com.badlogic.gdx.utils.Align
|
||||||
import com.unciv.Constants
|
import com.unciv.Constants
|
||||||
@ -24,10 +26,10 @@ import com.unciv.ui.components.extensions.addSeparator
|
|||||||
import com.unciv.ui.components.extensions.brighten
|
import com.unciv.ui.components.extensions.brighten
|
||||||
import com.unciv.ui.components.extensions.center
|
import com.unciv.ui.components.extensions.center
|
||||||
import com.unciv.ui.components.extensions.darken
|
import com.unciv.ui.components.extensions.darken
|
||||||
import com.unciv.ui.components.input.onClick
|
|
||||||
import com.unciv.ui.components.extensions.surroundWithCircle
|
import com.unciv.ui.components.extensions.surroundWithCircle
|
||||||
import com.unciv.ui.components.extensions.toLabel
|
import com.unciv.ui.components.extensions.toLabel
|
||||||
import com.unciv.ui.components.extensions.toPrettyString
|
import com.unciv.ui.components.extensions.toPrettyString
|
||||||
|
import com.unciv.ui.components.input.onClick
|
||||||
import com.unciv.ui.images.IconTextButton
|
import com.unciv.ui.images.IconTextButton
|
||||||
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
|
||||||
@ -258,17 +260,22 @@ class UnitOverviewTab(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unit.promotions.canBePromoted())
|
val canPromoteCell: Cell<Image>? =
|
||||||
promotionsTable.add(
|
if (unit.promotions.canBePromoted())
|
||||||
ImageGetter.getImage("OtherIcons/Star").apply {
|
promotionsTable.add(
|
||||||
color = if (GUI.isAllowedChangeState() && unit.currentMovement > 0f && unit.attacksThisTurn == 0)
|
ImageGetter.getImage("OtherIcons/Star").apply {
|
||||||
Color.GOLDENROD
|
color = if (GUI.isAllowedChangeState() && unit.currentMovement > 0f && unit.attacksThisTurn == 0)
|
||||||
else Color.GOLDENROD.darken(0.25f)
|
Color.GOLDENROD
|
||||||
}
|
else Color.GOLDENROD.darken(0.25f)
|
||||||
).size(24f).padLeft(8f)
|
}
|
||||||
|
).size(24f).padLeft(8f)
|
||||||
|
else null
|
||||||
promotionsTable.onClick {
|
promotionsTable.onClick {
|
||||||
if (unit.promotions.canBePromoted() || unit.promotions.promotions.isNotEmpty()) {
|
if (unit.promotions.canBePromoted() || unit.promotions.promotions.isNotEmpty()) {
|
||||||
game.pushScreen(PromotionPickerScreen(unit))
|
game.pushScreen(PromotionPickerScreen(unit) {
|
||||||
|
if (canPromoteCell != null && !unit.promotions.canBePromoted())
|
||||||
|
canPromoteCell.size(0f).pad(0f).setActor(null)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
add(promotionsTable)
|
add(promotionsTable)
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
package com.unciv.ui.screens.pickerscreens
|
||||||
|
|
||||||
|
import com.badlogic.gdx.scenes.scene2d.Touchable
|
||||||
|
import com.badlogic.gdx.scenes.scene2d.ui.Label
|
||||||
|
import com.badlogic.gdx.utils.Align
|
||||||
|
import com.unciv.models.ruleset.unit.Promotion
|
||||||
|
import com.unciv.ui.components.BorderedTable
|
||||||
|
import com.unciv.ui.components.extensions.toLabel
|
||||||
|
import com.unciv.ui.images.ImageGetter
|
||||||
|
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||||
|
|
||||||
|
internal class PromotionButton(
|
||||||
|
val node: PromotionTree.PromotionNode,
|
||||||
|
val isPickable: Boolean,
|
||||||
|
private val adoptedLabelStyle: Label.LabelStyle,
|
||||||
|
maxWidth: Float
|
||||||
|
) : BorderedTable(
|
||||||
|
path="PromotionScreen/PromotionButton",
|
||||||
|
defaultBgShape = BaseScreen.skinStrings.roundedEdgeRectangleMidShape,
|
||||||
|
defaultBgBorder = BaseScreen.skinStrings.roundedEdgeRectangleMidBorderShape
|
||||||
|
) {
|
||||||
|
private val label = node.promotion.name.toLabel(hideIcons = true)
|
||||||
|
private val defaultLabelStyle = label.style
|
||||||
|
private val colors = BaseScreen.skin[PromotionScreenColors::class.java]
|
||||||
|
|
||||||
|
init {
|
||||||
|
|
||||||
|
touchable = Touchable.enabled
|
||||||
|
borderSize = 5f
|
||||||
|
|
||||||
|
pad(5f)
|
||||||
|
align(Align.left)
|
||||||
|
add(ImageGetter.getPromotionPortrait(node.promotion.name)).padRight(10f)
|
||||||
|
label.setEllipsis(true)
|
||||||
|
add(label).left().maxWidth(maxWidth)
|
||||||
|
|
||||||
|
updateColor(false, emptySet(), emptySet())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateColor(isSelected: Boolean, pathToSelection: Set<Promotion>, prerequisites: Set<PromotionTree.PromotionNode>) {
|
||||||
|
bgColor = when {
|
||||||
|
isSelected -> colors.selected
|
||||||
|
node.isAdopted -> colors.promoted
|
||||||
|
node.promotion in pathToSelection -> colors.pathToSelection
|
||||||
|
node in prerequisites -> colors.prerequisite
|
||||||
|
isPickable -> colors.pickable
|
||||||
|
else -> colors.default
|
||||||
|
}
|
||||||
|
|
||||||
|
label.style = if (!isSelected && node.isAdopted) adoptedLabelStyle
|
||||||
|
else defaultLabelStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -3,142 +3,41 @@ package com.unciv.ui.screens.pickerscreens
|
|||||||
import com.badlogic.gdx.graphics.Color
|
import com.badlogic.gdx.graphics.Color
|
||||||
import com.badlogic.gdx.math.Vector2
|
import com.badlogic.gdx.math.Vector2
|
||||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||||
import com.badlogic.gdx.scenes.scene2d.Touchable
|
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.Cell
|
import com.badlogic.gdx.scenes.scene2d.ui.Cell
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.Image
|
import com.badlogic.gdx.scenes.scene2d.ui.Image
|
||||||
|
import com.badlogic.gdx.scenes.scene2d.ui.Label
|
||||||
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.logic.map.mapunit.MapUnit
|
import com.unciv.logic.map.mapunit.MapUnit
|
||||||
import com.unciv.models.TutorialTrigger
|
import com.unciv.models.TutorialTrigger
|
||||||
import com.unciv.models.UncivSound
|
import com.unciv.models.UncivSound
|
||||||
import com.unciv.models.ruleset.unit.Promotion
|
import com.unciv.models.ruleset.unit.Promotion
|
||||||
import com.unciv.models.translations.tr
|
import com.unciv.models.translations.tr
|
||||||
import com.unciv.ui.components.BorderedTable
|
import com.unciv.ui.audio.SoundPlayer
|
||||||
import com.unciv.ui.components.extensions.colorFromRGB
|
|
||||||
import com.unciv.ui.components.extensions.darken
|
|
||||||
import com.unciv.ui.components.extensions.isEnabled
|
import com.unciv.ui.components.extensions.isEnabled
|
||||||
|
import com.unciv.ui.components.extensions.toTextButton
|
||||||
import com.unciv.ui.components.input.onClick
|
import com.unciv.ui.components.input.onClick
|
||||||
import com.unciv.ui.components.input.onDoubleClick
|
import com.unciv.ui.components.input.onDoubleClick
|
||||||
import com.unciv.ui.components.extensions.setFontColor
|
|
||||||
import com.unciv.ui.components.extensions.toLabel
|
|
||||||
import com.unciv.ui.components.extensions.toTextButton
|
|
||||||
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.screens.basescreen.RecreateOnResize
|
import com.unciv.ui.screens.basescreen.RecreateOnResize
|
||||||
import java.lang.Integer.max
|
import com.unciv.utils.Concurrency
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
class PromotionNode(val promotion: Promotion) {
|
class PromotionPickerScreen(
|
||||||
var maxDepth = 0
|
val unit: MapUnit,
|
||||||
|
private val onChange: (() -> Unit)? = null
|
||||||
/** How many level this promotion has */
|
) : PickerScreen(), RecreateOnResize {
|
||||||
var levels = 1
|
// Style stuff
|
||||||
|
private val colors = skin[PromotionScreenColors::class.java]
|
||||||
val successors: ArrayList<PromotionNode> = ArrayList()
|
private val promotedLabelStyle = Label.LabelStyle(skin[Label.LabelStyle::class.java]).apply {
|
||||||
val predecessors: ArrayList<PromotionNode> = ArrayList()
|
fontColor = colors.promotedText
|
||||||
|
|
||||||
val baseName = getBasePromotionName()
|
|
||||||
|
|
||||||
fun isRoot() : Boolean {
|
|
||||||
return predecessors.isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun calculateDepth(excludeNodes: ArrayList<PromotionNode>, currentDepth: Int) {
|
|
||||||
maxDepth = max(maxDepth, currentDepth)
|
|
||||||
excludeNodes.add(this)
|
|
||||||
successors.filter { !excludeNodes.contains(it) }.forEach { it.calculateDepth(excludeNodes,currentDepth+1) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBasePromotionName(): String {
|
|
||||||
val nameWithoutBrackets = promotion.name.replace("[", "").replace("]", "")
|
|
||||||
val level = when {
|
|
||||||
nameWithoutBrackets.endsWith(" I") -> 1
|
|
||||||
nameWithoutBrackets.endsWith(" II") -> 2
|
|
||||||
nameWithoutBrackets.endsWith(" III") -> 3
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
return nameWithoutBrackets.dropLast(if (level == 0) 0 else level + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
class CustomComparator(
|
|
||||||
private val baseNode: PromotionNode
|
|
||||||
) : Comparator<PromotionNode> {
|
|
||||||
override fun compare(a: PromotionNode, b: PromotionNode): Int {
|
|
||||||
val baseName = baseNode.baseName
|
|
||||||
val aName = a.baseName
|
|
||||||
val bName = b.baseName
|
|
||||||
return when (aName) {
|
|
||||||
baseName -> -1
|
|
||||||
bName -> 0
|
|
||||||
else -> 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private class PromotionButton(
|
|
||||||
val node: PromotionNode,
|
|
||||||
val isPickable: Boolean = true,
|
|
||||||
val isPromoted: Boolean = false
|
|
||||||
) : BorderedTable(
|
|
||||||
path="PromotionScreen/PromotionButton",
|
|
||||||
defaultBgShape = BaseScreen.skinStrings.roundedEdgeRectangleMidShape,
|
|
||||||
defaultBgBorder = BaseScreen.skinStrings.roundedEdgeRectangleMidBorderShape
|
|
||||||
) {
|
|
||||||
|
|
||||||
var isSelected = false
|
|
||||||
val label = node.promotion.name.toLabel(hideIcons = true).apply {
|
|
||||||
wrap = false
|
|
||||||
setAlignment(Align.left)
|
|
||||||
setEllipsis(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
|
|
||||||
touchable = Touchable.enabled
|
|
||||||
borderSize = 5f
|
|
||||||
|
|
||||||
pad(5f)
|
|
||||||
align(Align.left)
|
|
||||||
add(ImageGetter.getPromotionPortrait(node.promotion.name)).padRight(10f)
|
|
||||||
add(label).left().maxWidth(130f)
|
|
||||||
|
|
||||||
updateColor()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateColor() {
|
|
||||||
|
|
||||||
val color = when {
|
|
||||||
isSelected -> PromotionPickerScreen.Selected
|
|
||||||
isPickable -> PromotionPickerScreen.Pickable
|
|
||||||
isPromoted -> PromotionPickerScreen.Promoted
|
|
||||||
else -> PromotionPickerScreen.Default
|
|
||||||
}
|
|
||||||
|
|
||||||
bgColor = color
|
|
||||||
|
|
||||||
val textColor = when {
|
|
||||||
isSelected -> Color.WHITE
|
|
||||||
isPromoted -> PromotionPickerScreen.Promoted.cpy().darken(0.8f)
|
|
||||||
else -> Color.WHITE
|
|
||||||
}
|
|
||||||
label.setFontColor(textColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class PromotionPickerScreen(val unit: MapUnit) : PickerScreen(), RecreateOnResize {
|
|
||||||
|
|
||||||
companion object Colors {
|
|
||||||
val Default: Color = Color.BLACK
|
|
||||||
val Selected: Color = colorFromRGB(72, 147, 175)
|
|
||||||
val Promoted: Color = colorFromRGB(255, 215, 0).darken(0.2f)
|
|
||||||
val Pickable: Color = colorFromRGB(28, 80, 0)
|
|
||||||
val Prerequisite: Color = colorFromRGB(14, 92, 86)
|
|
||||||
}
|
}
|
||||||
|
private val buttonCellMaxWidth: Float
|
||||||
|
private val buttonCellMinWidth: Float
|
||||||
|
|
||||||
|
// Widgets
|
||||||
private val promotionsTable = Table()
|
private val promotionsTable = Table()
|
||||||
private val promotionToButton = LinkedHashMap<String, PromotionButton>()
|
private val promotionToButton = LinkedHashMap<String, PromotionButton>()
|
||||||
private var selectedPromotion: PromotionButton? = null
|
private var selectedPromotion: PromotionButton? = null
|
||||||
@ -150,138 +49,88 @@ class PromotionPickerScreen(val unit: MapUnit) : PickerScreen(), RecreateOnResiz
|
|||||||
private val canPromoteNow = canChangeState && canBePromoted &&
|
private val canPromoteNow = canChangeState && canBePromoted &&
|
||||||
unit.currentMovement > 0 && unit.attacksThisTurn == 0
|
unit.currentMovement > 0 && unit.attacksThisTurn == 0
|
||||||
|
|
||||||
|
// Logic
|
||||||
|
private val tree = PromotionTree(unit)
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setDefaultCloseAction()
|
setDefaultCloseAction()
|
||||||
|
|
||||||
if (canPromoteNow) {
|
if (canPromoteNow) {
|
||||||
rightSideButton.setText("Pick promotion".tr())
|
rightSideButton.setText("Pick promotion".tr())
|
||||||
rightSideButton.onClick(UncivSound.Promote) {
|
rightSideButton.onClick(UncivSound.Silent) {
|
||||||
if (selectedPromotion?.isPickable == true)
|
acceptPromotion(selectedPromotion)
|
||||||
acceptPromotion(selectedPromotion?.node)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
rightSideButton.isVisible = false
|
rightSideButton.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptionLabel.setText(updateDescriptionLabel())
|
updateDescriptionLabel()
|
||||||
|
|
||||||
val availablePromotionsGroup = Table()
|
if (canChangeState) {
|
||||||
availablePromotionsGroup.defaults().pad(5f)
|
//Always allow the user to rename the unit as many times as they like.
|
||||||
|
val renameButton = "Choose name for [${unit.name}]".toTextButton()
|
||||||
val unitType = unit.type
|
renameButton.onClick {
|
||||||
val promotionsForUnitType = unit.civ.gameInfo.ruleset.unitPromotions.values.filter {
|
UnitRenamePopup(this, unit) {
|
||||||
it.unitTypes.contains(unitType.name) || unit.promotions.promotions.contains(it.name)
|
game.replaceCurrentScreen(recreate())
|
||||||
}
|
|
||||||
//Always allow the user to rename the unit as many times as they like.
|
|
||||||
val renameButton = "Choose name for [${unit.name}]".toTextButton()
|
|
||||||
renameButton.isEnabled = true
|
|
||||||
|
|
||||||
renameButton.onClick {
|
|
||||||
if (!canChangeState) return@onClick
|
|
||||||
UnitRenamePopup(
|
|
||||||
screen = this,
|
|
||||||
unit = unit,
|
|
||||||
actionOnClose = {
|
|
||||||
game.replaceCurrentScreen(PromotionPickerScreen(unit))
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
topTable.add(renameButton).pad(5f).row()
|
||||||
}
|
}
|
||||||
availablePromotionsGroup.add(renameButton)
|
|
||||||
|
|
||||||
topTable.add(availablePromotionsGroup).row()
|
// Create all buttons without placing them yet, measure
|
||||||
fillTable(promotionsForUnitType)
|
buttonCellMaxWidth = ((stage.width - 80f) / tree.getMaxColumns())
|
||||||
|
.coerceIn(190f, 300f)
|
||||||
|
for (node in tree.allNodes())
|
||||||
|
promotionToButton[node.promotion.name] = getButton(tree, node)
|
||||||
|
buttonCellMinWidth = (promotionToButton.values.maxOfOrNull { it.prefWidth + 10f } ?: 0f)
|
||||||
|
.coerceIn(190f, buttonCellMaxWidth)
|
||||||
|
|
||||||
|
fillTable()
|
||||||
|
|
||||||
displayTutorial(TutorialTrigger.Experience)
|
displayTutorial(TutorialTrigger.Experience)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun acceptPromotion(node: PromotionNode?) {
|
private fun acceptPromotion(button: PromotionButton?) {
|
||||||
// if user managed to click disabled button, still do nothing
|
// if user managed to click disabled button, still do nothing
|
||||||
if (node == null) return
|
if (button == null || !button.isPickable) return
|
||||||
|
|
||||||
unit.promotions.addPromotion(node.promotion.name)
|
// Can't use stage.addAction as the screen is going to die immediately
|
||||||
game.replaceCurrentScreen(recreate())
|
val path = tree.getPathTo(button.node.promotion)
|
||||||
}
|
if (path.size == 1) {
|
||||||
|
Concurrency.runOnGLThread { SoundPlayer.play(UncivSound.Promote) }
|
||||||
private fun fillTable(promotions: Collection<Promotion>) {
|
} else {
|
||||||
val map = LinkedHashMap<String, PromotionNode>()
|
Concurrency.runOnGLThread {
|
||||||
|
SoundPlayer.play(UncivSound.Promote)
|
||||||
val availablePromotions = unit.promotions.getAvailablePromotions()
|
Concurrency.run {
|
||||||
|
delay(200)
|
||||||
// Create nodes
|
Concurrency.runOnGLThread { SoundPlayer.play(UncivSound.Promote) }
|
||||||
// Pass 1 - create nodes for all promotions
|
|
||||||
for (promotion in promotions)
|
|
||||||
map[promotion.name] = PromotionNode(promotion)
|
|
||||||
|
|
||||||
// Pass 2 - remove nodes which are unreachable (dependent only on absent promotions)
|
|
||||||
for (promotion in promotions) {
|
|
||||||
if (promotion.prerequisites.isNotEmpty()) {
|
|
||||||
val isReachable = promotion.prerequisites.any { map.containsKey(it) }
|
|
||||||
if (!isReachable)
|
|
||||||
map.remove(promotion.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass 3 - fill nodes successors/predecessors, based on promotions prerequisites
|
|
||||||
for (node in map.values) {
|
|
||||||
for (prerequisiteName in node.promotion.prerequisites) {
|
|
||||||
val prerequisiteNode = map[prerequisiteName]
|
|
||||||
if (prerequisiteNode != null) {
|
|
||||||
node.predecessors.add(prerequisiteNode)
|
|
||||||
prerequisiteNode.successors.add(node)
|
|
||||||
// Prerequisite has the same base name -> +1 more level
|
|
||||||
if (prerequisiteNode.baseName == node.baseName)
|
|
||||||
prerequisiteNode.levels += 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Traverse each root node tree and calculate max possible depths of each node
|
for (promotion in path)
|
||||||
for (node in map.values) {
|
unit.promotions.addPromotion(promotion.name)
|
||||||
if (node.isRoot())
|
|
||||||
node.calculateDepth(arrayListOf(node), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For each non-root node remove all predecessors except the one with the least max depth.
|
onChange?.invoke()
|
||||||
// This is needed to compactify trees and remove circular dependencies (A -> B -> C -> A)
|
game.replaceCurrentScreen(recreate())
|
||||||
for (node in map.values) {
|
}
|
||||||
if (node.isRoot())
|
|
||||||
continue
|
|
||||||
|
|
||||||
// Choose best predecessor - the one with less depth
|
private fun fillTable() {
|
||||||
var best: PromotionNode? = null
|
val placedButtons = mutableSetOf<String>()
|
||||||
for (predecessor in node.predecessors) {
|
|
||||||
if (best == null || predecessor.maxDepth < best.maxDepth)
|
|
||||||
best = predecessor
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove everything else, leave only best
|
|
||||||
for (predecessor in node.predecessors)
|
|
||||||
predecessor.successors.remove(node)
|
|
||||||
node.predecessors.clear()
|
|
||||||
node.predecessors.add(best!!)
|
|
||||||
best.successors.add(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort nodes successors so promotions with same base name go first
|
|
||||||
for (node in map.values) {
|
|
||||||
node.successors.sortWith(PromotionNode.CustomComparator(node))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create cell matrix
|
// Create cell matrix
|
||||||
val maxColumns = map.size + 1
|
val maxColumns = tree.getMaxColumns()
|
||||||
val maxRows = map.size + 1
|
val maxRows = tree.getMaxRows()
|
||||||
|
val cellMatrix = Array(maxRows + 1) {
|
||||||
val cellMatrix = ArrayList<ArrayList<Cell<Actor>>>()
|
Array(maxColumns + 1) {
|
||||||
for (y in 0..maxRows) {
|
promotionsTable.add() as Cell<Actor?>
|
||||||
cellMatrix.add(ArrayList())
|
}.also {
|
||||||
for (x in 0..maxColumns) {
|
promotionsTable.row()
|
||||||
val cell = promotionsTable.add()
|
|
||||||
cellMatrix[y].add(cell)
|
|
||||||
}
|
}
|
||||||
promotionsTable.row()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check whether cell is inhabited by actor already */
|
/** Check whether a horizontal range of cells is inhabited by any actor already */
|
||||||
fun isTherePlace(row: Int, col: Int, levels: Int) : Boolean {
|
fun isTherePlace(row: Int, col: Int, levels: Int) : Boolean {
|
||||||
for (i in 0 until levels) {
|
for (i in 0 until levels) {
|
||||||
if (cellMatrix[row][col+i].actor != null)
|
if (cellMatrix[row][col+i].actor != null)
|
||||||
@ -290,24 +139,17 @@ class PromotionPickerScreen(val unit: MapUnit) : PickerScreen(), RecreateOnResiz
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Recursively place buttons for node and it's successors into free cells */
|
/** Recursively place buttons for node and its successors into free cells */
|
||||||
fun placeButton(col: Int, row: Int, node: PromotionNode) : Int {
|
fun placeButton(col: Int, row: Int, node: PromotionTree.PromotionNode) : Int {
|
||||||
val name = node.promotion.name
|
val name = node.promotion.name
|
||||||
// If promotion button not yet placed
|
// If promotion button not yet placed
|
||||||
if (promotionToButton[name] == null) {
|
if (name !in placedButtons) {
|
||||||
// If place is free - we place button
|
// If place is free - we place button
|
||||||
if (isTherePlace(row, col, node.levels)) {
|
if (isTherePlace(row, col, node.levels)) {
|
||||||
val cell = cellMatrix[row][col]
|
cellMatrix[row][col].setActor(promotionToButton[name])
|
||||||
val isPromotionAvailable = node.promotion in availablePromotions
|
.pad(5f).padRight(20f)
|
||||||
val hasPromotion = unit.promotions.promotions.contains(name)
|
.minWidth(buttonCellMinWidth).maxWidth(buttonCellMaxWidth)
|
||||||
val isPickable = canPromoteNow && isPromotionAvailable && !hasPromotion
|
placedButtons += name
|
||||||
val button = getButton(promotions, node, isPickable, hasPromotion)
|
|
||||||
promotionToButton[name] = button
|
|
||||||
cell.setActor(button)
|
|
||||||
cell.pad(5f)
|
|
||||||
cell.padRight(20f)
|
|
||||||
cell.minWidth(190f)
|
|
||||||
cell.maxWidth(190f)
|
|
||||||
}
|
}
|
||||||
// If place is not free - try to find another in the next row
|
// If place is not free - try to find another in the next row
|
||||||
else {
|
else {
|
||||||
@ -315,79 +157,82 @@ class PromotionPickerScreen(val unit: MapUnit) : PickerScreen(), RecreateOnResiz
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter successors who haven't been placed yet (to avoid circular dependencies)
|
// Filter children who haven't been placed yet (to avoid circular dependencies)
|
||||||
// and try to place them in the next column.
|
// and try to place them in the next column.
|
||||||
|
// Note this materializes all intermediaries as Lists, but they're small
|
||||||
|
// Also note having placeButton with nointrivial side effecths in a chain isn't good practice,
|
||||||
|
// But the alternative is coding the max manually.
|
||||||
// Return the max row this whole tree ever reached.
|
// Return the max row this whole tree ever reached.
|
||||||
return node.successors.filter {
|
return node.children
|
||||||
!promotionToButton.containsKey(it.promotion.name)
|
.filter { it.promotion.name !in placedButtons }
|
||||||
}.map {
|
.sortedBy { it.baseName != node.baseName } // Prioritize getting groups in a row - relying on sensible json "column" isn't enough
|
||||||
placeButton(col+1, row, it)
|
.maxOfOrNull { placeButton(col + 1, row, it) }
|
||||||
}.maxOfOrNull { it }?: row
|
?: row
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build each tree starting from root nodes
|
// Build each tree starting from root nodes
|
||||||
var row = 0
|
var row = 0
|
||||||
for (node in map.values) {
|
for (node in tree.allRoots()) {
|
||||||
if (node.isRoot()) {
|
row = placeButton(0, row, node)
|
||||||
row = placeButton(0, row, node)
|
// Each root tree should start from a completely empty row.
|
||||||
// Each root tree should start from a completely empty row.
|
row += 1
|
||||||
row += 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
topTable.add(promotionsTable)
|
topTable.add(promotionsTable)
|
||||||
|
addConnectingLines(emptySet())
|
||||||
addConnectingLines()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getButton(allPromotions: Collection<Promotion>, node: PromotionNode,
|
private fun getButton(tree: PromotionTree, node: PromotionTree.PromotionNode) : PromotionButton {
|
||||||
isPickable: Boolean = true, isPromoted: Boolean = false) : PromotionButton {
|
val isPickable = (!node.pathIsAmbiguous || node.distanceToAdopted == 1) && tree.canBuyUpTo(node.promotion)
|
||||||
|
|
||||||
val button = PromotionButton(
|
val button = PromotionButton(node, isPickable, promotedLabelStyle, buttonCellMaxWidth - 60f)
|
||||||
node = node,
|
|
||||||
isPromoted = isPromoted,
|
|
||||||
isPickable = isPickable
|
|
||||||
)
|
|
||||||
|
|
||||||
button.onClick {
|
button.onClick {
|
||||||
selectedPromotion?.isSelected = false
|
|
||||||
selectedPromotion?.updateColor()
|
|
||||||
selectedPromotion = button
|
selectedPromotion = button
|
||||||
button.isSelected = true
|
|
||||||
button.updateColor()
|
val path = tree.getPathTo(button.node.promotion)
|
||||||
|
val pathAsSet = path.toSet()
|
||||||
|
val prerequisites = button.node.parents
|
||||||
|
|
||||||
for (btn in promotionToButton.values)
|
for (btn in promotionToButton.values)
|
||||||
btn.updateColor()
|
btn.updateColor(btn == selectedPromotion, pathAsSet, prerequisites)
|
||||||
button.node.promotion.prerequisites.forEach { promotionToButton[it]?.apply {
|
|
||||||
if (!this.isPromoted)
|
|
||||||
bgColor = Prerequisite }}
|
|
||||||
|
|
||||||
rightSideButton.isEnabled = isPickable
|
rightSideButton.isEnabled = isPickable
|
||||||
rightSideButton.setText(node.promotion.name.tr())
|
rightSideButton.setText(node.promotion.name.tr())
|
||||||
descriptionLabel.setText(updateDescriptionLabel(node.promotion.getDescription(allPromotions)))
|
updateDescriptionLabel(isPickable, tree, node, path)
|
||||||
|
|
||||||
addConnectingLines()
|
addConnectingLines(pathAsSet)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPickable)
|
if (isPickable)
|
||||||
button.onDoubleClick(UncivSound.Promote) {
|
button.onDoubleClick(UncivSound.Silent) {
|
||||||
acceptPromotion(node)
|
acceptPromotion(button)
|
||||||
}
|
}
|
||||||
|
|
||||||
return button
|
return button
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addConnectingLines() {
|
private fun addConnectingLines(path: Set<Promotion>) {
|
||||||
promotionsTable.pack()
|
promotionsTable.pack()
|
||||||
scrollPane.updateVisualScroll()
|
scrollPane.updateVisualScroll()
|
||||||
|
|
||||||
for (line in lines) line.remove()
|
for (line in lines) line.remove()
|
||||||
lines.clear()
|
lines.clear()
|
||||||
|
|
||||||
|
fun addLine(x: Float, y: Float, width: Float, height: Float, color: Color) {
|
||||||
|
if (color.a == 0f) return
|
||||||
|
val line = ImageGetter.getWhiteDot()
|
||||||
|
line.setBounds(x, y, width, height)
|
||||||
|
line.color = color
|
||||||
|
promotionsTable.addActorAt(0, line)
|
||||||
|
lines.add(line)
|
||||||
|
}
|
||||||
|
|
||||||
for (button in promotionToButton.values) {
|
for (button in promotionToButton.values) {
|
||||||
for (prerequisite in button.node.promotion.prerequisites) {
|
val currentNode = button.node
|
||||||
|
for (prerequisite in currentNode.promotion.prerequisites) {
|
||||||
val prerequisiteButton = promotionToButton[prerequisite] ?: continue
|
val prerequisiteButton = promotionToButton[prerequisite] ?: continue
|
||||||
|
val prerequisiteNode = prerequisiteButton.node
|
||||||
|
|
||||||
var buttonCoords = Vector2(0f, button.height / 2)
|
var buttonCoords = Vector2(0f, button.height / 2)
|
||||||
button.localToStageCoordinates(buttonCoords)
|
button.localToStageCoordinates(buttonCoords)
|
||||||
@ -397,15 +242,16 @@ class PromotionPickerScreen(val unit: MapUnit) : PickerScreen(), RecreateOnResiz
|
|||||||
prerequisiteButton.localToStageCoordinates(prerequisiteCoords)
|
prerequisiteButton.localToStageCoordinates(prerequisiteCoords)
|
||||||
promotionsTable.stageToLocalCoordinates(prerequisiteCoords)
|
promotionsTable.stageToLocalCoordinates(prerequisiteCoords)
|
||||||
|
|
||||||
|
val isNodeInPath = currentNode.promotion in path
|
||||||
|
val isSelectionPath = isNodeInPath &&
|
||||||
|
(prerequisiteNode.isAdopted || prerequisiteNode.promotion in path)
|
||||||
val lineColor = when {
|
val lineColor = when {
|
||||||
button.isSelected -> Selected
|
isSelectionPath -> colors.selected
|
||||||
prerequisiteButton.node.baseName == button.node.baseName -> Color.WHITE.cpy()
|
isNodeInPath -> colors.pathToSelection
|
||||||
else -> Color.CLEAR
|
prerequisiteNode.baseName == currentNode.baseName -> colors.groupLines
|
||||||
}
|
else -> colors.otherLines
|
||||||
val lineSize = when {
|
|
||||||
button.isSelected -> 4f
|
|
||||||
else -> 2f
|
|
||||||
}
|
}
|
||||||
|
val lineSize = if (isSelectionPath) 4f else 2f
|
||||||
|
|
||||||
if (buttonCoords.x < prerequisiteCoords.x) {
|
if (buttonCoords.x < prerequisiteCoords.x) {
|
||||||
val temp = buttonCoords.cpy()
|
val temp = buttonCoords.cpy()
|
||||||
@ -413,67 +259,48 @@ class PromotionPickerScreen(val unit: MapUnit) : PickerScreen(), RecreateOnResiz
|
|||||||
prerequisiteCoords = temp
|
prerequisiteCoords = temp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val halfLineSize = lineSize / 2
|
||||||
if (buttonCoords.y != prerequisiteCoords.y) {
|
if (buttonCoords.y != prerequisiteCoords.y) {
|
||||||
|
|
||||||
val deltaX = buttonCoords.x - prerequisiteCoords.x
|
val deltaX = buttonCoords.x - prerequisiteCoords.x
|
||||||
val deltaY = buttonCoords.y - prerequisiteCoords.y
|
val deltaY = buttonCoords.y - prerequisiteCoords.y
|
||||||
val halfLength = deltaX / 2f
|
val halfLength = deltaX / 2f + halfLineSize
|
||||||
|
|
||||||
val line = ImageGetter.getWhiteDot().apply {
|
|
||||||
width = halfLength+lineSize/2
|
|
||||||
height = lineSize
|
|
||||||
x = prerequisiteCoords.x
|
|
||||||
y = prerequisiteCoords.y - lineSize / 2
|
|
||||||
}
|
|
||||||
val line1 = ImageGetter.getWhiteDot().apply {
|
|
||||||
width = halfLength + lineSize/2
|
|
||||||
height = lineSize
|
|
||||||
x = buttonCoords.x - width
|
|
||||||
y = buttonCoords.y - lineSize / 2
|
|
||||||
}
|
|
||||||
val line2 = ImageGetter.getWhiteDot().apply {
|
|
||||||
width = lineSize
|
|
||||||
height = abs(deltaY)
|
|
||||||
x = buttonCoords.x - halfLength - lineSize / 2
|
|
||||||
y = buttonCoords.y + (if (deltaY > 0f) -height-lineSize/2 else lineSize/2)
|
|
||||||
}
|
|
||||||
|
|
||||||
line.color = lineColor
|
|
||||||
line1.color = lineColor
|
|
||||||
line2.color = lineColor
|
|
||||||
|
|
||||||
promotionsTable.addActor(line)
|
|
||||||
promotionsTable.addActor(line1)
|
|
||||||
promotionsTable.addActor(line2)
|
|
||||||
|
|
||||||
line.toBack()
|
|
||||||
line1.toBack()
|
|
||||||
line2.toBack()
|
|
||||||
|
|
||||||
lines.add(line)
|
|
||||||
lines.add(line1)
|
|
||||||
lines.add(line2)
|
|
||||||
|
|
||||||
|
addLine(
|
||||||
|
width = halfLength,
|
||||||
|
height = lineSize,
|
||||||
|
x = prerequisiteCoords.x,
|
||||||
|
y = prerequisiteCoords.y - halfLineSize,
|
||||||
|
color = lineColor
|
||||||
|
)
|
||||||
|
addLine(
|
||||||
|
width = halfLength,
|
||||||
|
height = lineSize,
|
||||||
|
x = buttonCoords.x - halfLength,
|
||||||
|
y = buttonCoords.y - halfLineSize,
|
||||||
|
color = lineColor
|
||||||
|
)
|
||||||
|
addLine(
|
||||||
|
width = lineSize,
|
||||||
|
height = abs(deltaY),
|
||||||
|
x = buttonCoords.x - halfLength,
|
||||||
|
y = buttonCoords.y + (if (deltaY > 0f) -deltaY - halfLineSize else halfLineSize),
|
||||||
|
color = lineColor
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
|
addLine(
|
||||||
val line = ImageGetter.getWhiteDot().apply {
|
width = buttonCoords.x - prerequisiteCoords.x,
|
||||||
width = buttonCoords.x - prerequisiteCoords.x
|
height = lineSize,
|
||||||
height = lineSize
|
x = prerequisiteCoords.x,
|
||||||
x = prerequisiteCoords.x
|
y = prerequisiteCoords.y - halfLineSize,
|
||||||
y = prerequisiteCoords.y - lineSize / 2
|
color = lineColor
|
||||||
}
|
)
|
||||||
line.color = lineColor
|
|
||||||
promotionsTable.addActor(line)
|
|
||||||
line.toBack()
|
|
||||||
lines.add(line)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (line in lines) {
|
for (line in lines) {
|
||||||
if (line.color == Selected)
|
if (line.color == colors.selected || line.color == colors.pathToSelection)
|
||||||
line.zIndex = lines.size
|
line.zIndex = lines.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -484,18 +311,29 @@ class PromotionPickerScreen(val unit: MapUnit) : PickerScreen(), RecreateOnResiz
|
|||||||
scrollPane.updateVisualScroll()
|
scrollPane.updateVisualScroll()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateDescriptionLabel(): String {
|
private fun updateDescriptionLabel() {
|
||||||
return unit.displayName().tr()
|
descriptionLabel.setText(unit.displayName().tr())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateDescriptionLabel(promotionDescription: String): String {
|
private fun updateDescriptionLabel(
|
||||||
var newDescriptionText = unit.displayName().tr()
|
isPickable: Boolean,
|
||||||
newDescriptionText += "\n" + promotionDescription
|
tree: PromotionTree,
|
||||||
return newDescriptionText
|
node: PromotionTree.PromotionNode,
|
||||||
|
path: List<Promotion>
|
||||||
|
) {
|
||||||
|
val isAmbiguous = node.pathIsAmbiguous && node.distanceToAdopted > 1 && tree.canBuyUpTo(node.promotion)
|
||||||
|
val topLine = unit.displayName().tr() + when {
|
||||||
|
node.isAdopted -> ""
|
||||||
|
isAmbiguous -> " - {Path to [${node.promotion.name}] is ambiguous}".tr()
|
||||||
|
!isPickable -> ""
|
||||||
|
else -> path.joinToString(" → ", ": ") { it.name.tr() }
|
||||||
|
}
|
||||||
|
val promotionText = node.promotion.getDescription(tree.possiblePromotions)
|
||||||
|
descriptionLabel.setText("$topLine\n$promotionText")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recreate(): BaseScreen {
|
override fun recreate(): BaseScreen {
|
||||||
val newScreen = PromotionPickerScreen(unit)
|
val newScreen = PromotionPickerScreen(unit, onChange)
|
||||||
newScreen.setScrollY(scrollPane.scrollY)
|
newScreen.setScrollY(scrollPane.scrollY)
|
||||||
return newScreen
|
return newScreen
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
package com.unciv.ui.screens.pickerscreens
|
||||||
|
|
||||||
|
import com.badlogic.gdx.graphics.Color
|
||||||
|
|
||||||
|
|
||||||
|
/** Colours used on the [PromotionPickerScreen]
|
||||||
|
*
|
||||||
|
* These are backed by Skin.json
|
||||||
|
*/
|
||||||
|
class PromotionScreenColors {
|
||||||
|
val default: Color = Color.BLACK
|
||||||
|
val selected: Color = Color(0.2824f, 0.5765f, 0.6863f, 1f) // colorFromRGB(72, 147, 175)
|
||||||
|
val pathToSelection: Color = Color(0.1882f, 0.3843f, 0.4575f, 1f) // selected.darken(0.33f)
|
||||||
|
val promoted: Color = Color(0.8f, 0.6745f, 0f, 1f) // colorFromRGB(255, 215, 0).darken(0.2f)
|
||||||
|
val promotedText: Color = Color(0.16f, 0.1349f, 0f, 1f) // promoted.darken(0.8f)
|
||||||
|
val pickable: Color = Color(0.1098f, 0.3137f, 0f, 1f) // colorFromRGB(28, 80, 0)
|
||||||
|
val prerequisite: Color = Color(0.4f, 0.5f, 0.8f, 1f) // HSV(225,50,80): muted Royal
|
||||||
|
val groupLines: Color = Color.WHITE
|
||||||
|
val otherLines: Color = Color.CLEAR
|
||||||
|
}
|
183
core/src/com/unciv/ui/screens/pickerscreens/PromotionTree.kt
Normal file
183
core/src/com/unciv/ui/screens/pickerscreens/PromotionTree.kt
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
package com.unciv.ui.screens.pickerscreens
|
||||||
|
|
||||||
|
import com.unciv.GUI
|
||||||
|
import com.unciv.logic.map.mapunit.MapUnit
|
||||||
|
import com.unciv.models.ruleset.unique.StateForConditionals
|
||||||
|
import com.unciv.models.ruleset.unique.UniqueType
|
||||||
|
import com.unciv.models.ruleset.unit.Promotion
|
||||||
|
import com.unciv.models.translations.tr
|
||||||
|
import com.unciv.utils.Log
|
||||||
|
|
||||||
|
internal class PromotionTree(val unit: MapUnit) {
|
||||||
|
/** Ordered set of Promotions to show - by Json column/row and translated name */
|
||||||
|
// Not using SortedSet - that uses needlessly complex implementations that remember the comparator
|
||||||
|
val possiblePromotions: LinkedHashSet<Promotion>
|
||||||
|
/** Ordered map, key is the Promotion name, same order as [possiblePromotions] */
|
||||||
|
private val nodes: LinkedHashMap<String, PromotionNode>
|
||||||
|
|
||||||
|
class PromotionNode(
|
||||||
|
val promotion: Promotion,
|
||||||
|
val isAdopted: Boolean
|
||||||
|
) {
|
||||||
|
/** How many prerequisite steps are needed to reach a [isRoot] promotion */
|
||||||
|
var depth = Int.MIN_VALUE
|
||||||
|
/** How many unit-promoting steps are needed to reach this node */
|
||||||
|
var distanceToAdopted = Int.MAX_VALUE
|
||||||
|
|
||||||
|
/** The nodes for direct prerequisites of this one (unordered)
|
||||||
|
* Note this is not necessarily cover all prerequisites of the node's promotion - see [unreachable] */
|
||||||
|
val parents = mutableSetOf<PromotionNode>()
|
||||||
|
/** Follow this to get an unambiguous path to a root */
|
||||||
|
var preferredParent: PromotionNode? = null
|
||||||
|
/** All nodes having this one as direct prerequisite - must preserve order as UI uses it */
|
||||||
|
val children = linkedSetOf<PromotionNode>()
|
||||||
|
|
||||||
|
/** Off if there is only one "best" path of equal cost to adopt this node's promotion */
|
||||||
|
var pathIsAmbiguous = false
|
||||||
|
/** On for promotions having unavailable prerequisites (missing in ruleset, or not allowed for the unit's
|
||||||
|
* UnitType, and not already adopted either); or currently disabled by a [UniqueType.OnlyAvailableWhen] unique.
|
||||||
|
* (should never be on with a vanilla ruleset) */
|
||||||
|
var unreachable = false
|
||||||
|
|
||||||
|
/** Name of this node's promotion with [level] suffixes removed, and [] brackets removed */
|
||||||
|
val baseName: String
|
||||||
|
/** "Level" of this node's promotion (e.g. Drill I: 1, Drill III: 3 - 0 for promotions without such a suffix) */
|
||||||
|
val level: Int
|
||||||
|
/** How many levels of this promotion there are below (including this), minimum 1 (Drill I: 3 / Drill III: 1) */
|
||||||
|
var levels = 1
|
||||||
|
|
||||||
|
/** `true` if this node's promotion has no prerequisites */
|
||||||
|
val isRoot get() = parents.isEmpty()
|
||||||
|
|
||||||
|
override fun toString() = promotion.name
|
||||||
|
|
||||||
|
init {
|
||||||
|
val splitName = Promotion.getBaseNameAndLevel(promotion.name)
|
||||||
|
this.level = splitName.level
|
||||||
|
this.baseName = splitName.basePromotionName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
val collator = GUI.getSettings().getCollatorFromLocale()
|
||||||
|
val rulesetPromotions = unit.civ.gameInfo.ruleset.unitPromotions.values
|
||||||
|
val unitType = unit.baseUnit.unitType
|
||||||
|
val adoptedPromotions = unit.promotions.promotions
|
||||||
|
|
||||||
|
// The following sort is mostly redundant with our vanilla rulesets.
|
||||||
|
// Still, want to make sure processing left to right, top to bottom will be usable.
|
||||||
|
possiblePromotions = rulesetPromotions.asSequence()
|
||||||
|
.filter {
|
||||||
|
unitType in it.unitTypes || it.name in adoptedPromotions
|
||||||
|
}
|
||||||
|
.sortedWith(
|
||||||
|
// Remember to make sure row=0/col=0 stays on top while those without explicit pos go to the end
|
||||||
|
// Also remember the names are historical, row means column on our current screen design.
|
||||||
|
compareBy<Promotion> {
|
||||||
|
if (it.row < 0) Int.MAX_VALUE
|
||||||
|
else if (it.row == 0) Int.MIN_VALUE + it.column
|
||||||
|
else it.column
|
||||||
|
}
|
||||||
|
.thenBy { it.row }
|
||||||
|
.thenBy(collator) { it.name.tr(hideIcons = true) }
|
||||||
|
)
|
||||||
|
.toCollection(linkedSetOf())
|
||||||
|
|
||||||
|
// Create incomplete node objects
|
||||||
|
nodes = possiblePromotions.asSequence()
|
||||||
|
.map { it.name to PromotionNode(it, it.name in adoptedPromotions) }
|
||||||
|
.toMap(LinkedHashMap(possiblePromotions.size))
|
||||||
|
|
||||||
|
// Fill parent/child relations, ignoring prerequisites not in possiblePromotions
|
||||||
|
for (node in nodes.values) {
|
||||||
|
for (prerequisite in node.promotion.prerequisites) {
|
||||||
|
val parent = nodes[prerequisite] ?: continue
|
||||||
|
if (node in allChildren(parent)) {
|
||||||
|
Log.debug("Ignoring circular reference: %s requires %s", node, parent)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
node.parents += parent
|
||||||
|
parent.children += node
|
||||||
|
if (node.level > 0 && node.baseName == parent.baseName)
|
||||||
|
parent.levels++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine unreachable / disabled nodes
|
||||||
|
val state = StateForConditionals(unit.civ, unit = unit, tile = unit.getTile())
|
||||||
|
for (node in nodes.values) {
|
||||||
|
// defensive - I don't know how to provoke the situation, but if it ever occurs, disallow choosing that promotion
|
||||||
|
if (node.promotion.prerequisites.isNotEmpty() && node.parents.isEmpty())
|
||||||
|
node.unreachable = true
|
||||||
|
if (node.promotion.getMatchingUniques(UniqueType.OnlyAvailableWhen, StateForConditionals.IgnoreConditionals)
|
||||||
|
.any { !it.conditionalsApply(state) })
|
||||||
|
node.unreachable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate depth and distanceToAdopted - nonrecursively, shallows first.
|
||||||
|
// Also determine preferredParent / pathIsAmbiguous by weighing distanceToAdopted
|
||||||
|
for (node in allRoots()) {
|
||||||
|
node.depth = 0
|
||||||
|
node.distanceToAdopted = if (node.unreachable) Int.MAX_VALUE else if (node.isAdopted) 0 else 1
|
||||||
|
}
|
||||||
|
for (depth in 0..99) {
|
||||||
|
var complete = true
|
||||||
|
for (node in nodes.values) {
|
||||||
|
if (node.depth == Int.MIN_VALUE) {
|
||||||
|
complete = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (node.depth != depth) continue
|
||||||
|
for (child in node.children) {
|
||||||
|
val distance = if (node.distanceToAdopted == Int.MAX_VALUE) Int.MAX_VALUE
|
||||||
|
else if (child.isAdopted) 0 else node.distanceToAdopted + 1
|
||||||
|
when {
|
||||||
|
child.depth == Int.MIN_VALUE -> Unit // "New" node / first reached
|
||||||
|
child.distanceToAdopted < distance -> continue // Already reached a better way
|
||||||
|
child.distanceToAdopted == distance -> { // Already reached same distance
|
||||||
|
child.pathIsAmbiguous = true
|
||||||
|
child.preferredParent = null
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// else: Already reached, but a worse way - overwrite fully
|
||||||
|
}
|
||||||
|
child.depth = depth + 1
|
||||||
|
child.distanceToAdopted = distance
|
||||||
|
child.pathIsAmbiguous = node.pathIsAmbiguous
|
||||||
|
child.preferredParent = node.takeUnless { node.pathIsAmbiguous }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (complete) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun allNodes() = nodes.values.asSequence()
|
||||||
|
fun allRoots() = allNodes().filter { it.isRoot }
|
||||||
|
private fun allChildren(node: PromotionNode): Sequence<PromotionNode> {
|
||||||
|
return sequenceOf(node) + node.children.flatMap { allChildren(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getReachableNode(promotion: Promotion): PromotionNode? =
|
||||||
|
nodes[promotion.name]?.takeUnless { it.distanceToAdopted == Int.MAX_VALUE }
|
||||||
|
|
||||||
|
fun canBuyUpTo(promotion: Promotion): Boolean = unit.promotions.run {
|
||||||
|
val node = getReachableNode(promotion) ?: return false
|
||||||
|
if (node.isAdopted) return false
|
||||||
|
return XP >= xpForNextNPromotions(node.distanceToAdopted)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPathTo(promotion: Promotion): List<Promotion> {
|
||||||
|
var node = getReachableNode(promotion) ?: return emptyList()
|
||||||
|
val result = mutableListOf(node.promotion)
|
||||||
|
while (true) {
|
||||||
|
node = node.preferredParent ?: break
|
||||||
|
if (node.isAdopted) break
|
||||||
|
result.add(node.promotion)
|
||||||
|
}
|
||||||
|
return result.asReversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// These exist to allow future optimization - this is safe, but more than actually needed
|
||||||
|
fun getMaxRows() = nodes.size
|
||||||
|
fun getMaxColumns() = nodes.values.maxOf { it.promotion.row.coerceAtLeast(it.depth + 1) }
|
||||||
|
}
|
@ -24,4 +24,3 @@ class UnitRenamePopup(val screen: BaseScreen, val unit: MapUnit, val actionOnClo
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,14 +44,16 @@ Remember, promotions can be "bought" with XP, but also granted by the unit type,
|
|||||||
|
|
||||||
Each promotion can have the following properties:
|
Each promotion can have the following properties:
|
||||||
|
|
||||||
| Attribute | Type | Optional | Notes |
|
| Attribute | Type | Optional | Notes |
|
||||||
| --------- | ---- | -------- | ----- |
|
|-----------------|--------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| name | String | Required | See above for "I, II, III" progressions |
|
| name | String | Required | See above for "I, II, III" progressions |
|
||||||
| prerequisites | List | Default empty | Prerequisite promotions |
|
| prerequisites | List | Default empty | Prerequisite promotions |
|
||||||
| effect | String | Default empty | Deprecated, use uniques instead |
|
| effect | String | Default empty | Deprecated and ignored, use uniques instead |
|
||||||
| unitTypes | List | Default empty | The unit types for which this promotion applies as specified in [UnitTypes.json](#unittypesjson) |
|
| column | Int | Yes | Determines placement order on the promotion picker screen. Name is historical, these coordinates no longer control placement directly. Promotions without coordinates are ensured to be placed last. (…) |
|
||||||
| uniques | List | Default empty | List of effects, [see here](../Modders/Unique-parameters.md#unit-uniques) |
|
| row | Int | Yes | … In base mods without any coordinates, promotions without prerequisites are sorted alphabetically and placed top down, the rest of the screen will structure the dependencies logically. If your mod has a "Heal instantly", it is suggested to use row=0 to place it on top. |
|
||||||
| civilopediaText | List | Default empty | see [civilopediaText chapter](Miscellaneous-JSON-files.md#civilopedia-text) |
|
| unitTypes | List | Default empty | The unit types for which this promotion applies as specified in [UnitTypes.json](#unittypesjson) |
|
||||||
|
| uniques | List | Default empty | List of effects, [see here](../Modders/uniques.md#unit-uniques) |
|
||||||
|
| civilopediaText | List | Default empty | see [civilopediaText chapter](Miscellaneous-JSON-files.md#civilopedia-text) |
|
||||||
|
|
||||||
## UnitTypes.json
|
## UnitTypes.json
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user