mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-29 23:10:39 -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,
|
||||
"b": 0.039215688,
|
||||
"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": {
|
||||
@ -319,5 +373,18 @@
|
||||
"cursor": "white",
|
||||
"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]
|
||||
[unitFilter] units gain the [promotion] promotion = [unitFilter] Einheiten erhalten die [promotion] Beförderung
|
||||
Requires = Benötigt
|
||||
Path to [promotion] is ambiguous = Der Weg zu [promotion] ist noch nicht klar
|
||||
|
||||
# Multiplayer Turn Checker Service
|
||||
|
||||
|
@ -1684,6 +1684,7 @@ Dogfighting III =
|
||||
Choose name for [unitName] =
|
||||
[unitFilter] units gain the [promotion] promotion =
|
||||
Requires =
|
||||
Path to [promotion] is ambiguous =
|
||||
|
||||
# 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,... */
|
||||
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 */
|
||||
fun totalXpProduced() = XP + (numberOfPromotions * (numberOfPromotions + 1)) * 5
|
||||
|
||||
|
@ -162,7 +162,7 @@ class Ruleset {
|
||||
cityStateTypes.putAll(ruleset.cityStateTypes)
|
||||
ruleset.modOptions.unitsToRemove
|
||||
.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 {
|
||||
units.remove(it)
|
||||
}
|
||||
@ -170,30 +170,7 @@ class Ruleset {
|
||||
modOptions.uniques.addAll(ruleset.modOptions.uniques)
|
||||
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
|
||||
// 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)
|
||||
unitPromotions.putAll(ruleset.unitPromotions)
|
||||
|
||||
mods += ruleset.mods
|
||||
}
|
||||
@ -312,14 +289,6 @@ class Ruleset {
|
||||
val promotionsFile = folderHandle.child("UnitPromotions.json")
|
||||
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")
|
||||
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.translations.tr
|
||||
import com.unciv.ui.screens.civilopediascreen.FormattedLine
|
||||
|
||||
import com.unciv.ui.screens.pickerscreens.PromotionPickerScreen
|
||||
|
||||
class Promotion : RulesetObject() {
|
||||
var prerequisites = listOf<String>()
|
||||
|
||||
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
|
||||
/** 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
|
||||
|
||||
fun clone():Promotion {
|
||||
fun clone(): Promotion {
|
||||
val newPromotion = Promotion()
|
||||
|
||||
// RulesetObject fields
|
||||
@ -36,7 +41,7 @@ class Promotion : RulesetObject() {
|
||||
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 {
|
||||
val textList = ArrayList<String>()
|
||||
|
||||
@ -146,4 +151,27 @@ class Promotion : RulesetObject() {
|
||||
|
||||
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.utils.Align
|
||||
import com.unciv.models.ruleset.Ruleset
|
||||
import com.unciv.models.ruleset.unit.Promotion
|
||||
import com.unciv.models.stats.Stats
|
||||
import com.unciv.ui.components.extensions.center
|
||||
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 {
|
||||
val (nameWithoutBrackets, level, basePromotionName) = Promotion.getBaseNameAndLevel(imageName)
|
||||
|
||||
val nameWithoutBrackets = imageName.replace("[", "").replace("]", "")
|
||||
|
||||
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)
|
||||
|
||||
this.level = level
|
||||
val pathWithoutBrackets = "UnitPromotionIcons/$nameWithoutBrackets"
|
||||
val pathBase = "UnitPromotionIcons/$basePromotionName"
|
||||
val pathUnit = "UnitIcons/${basePromotionName.removeSuffix(" ability")}"
|
||||
|
||||
return when {
|
||||
ImageGetter.imageExists(pathWithoutBrackets) -> {
|
||||
level = 0
|
||||
this.level = 0
|
||||
ImageGetter.getImage(pathWithoutBrackets)
|
||||
}
|
||||
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.Group
|
||||
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.utils.Align
|
||||
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.center
|
||||
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.toLabel
|
||||
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.ImageGetter
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
@ -258,17 +260,22 @@ class UnitOverviewTab(
|
||||
}
|
||||
}
|
||||
|
||||
if (unit.promotions.canBePromoted())
|
||||
promotionsTable.add(
|
||||
ImageGetter.getImage("OtherIcons/Star").apply {
|
||||
color = if (GUI.isAllowedChangeState() && unit.currentMovement > 0f && unit.attacksThisTurn == 0)
|
||||
Color.GOLDENROD
|
||||
else Color.GOLDENROD.darken(0.25f)
|
||||
}
|
||||
).size(24f).padLeft(8f)
|
||||
val canPromoteCell: Cell<Image>? =
|
||||
if (unit.promotions.canBePromoted())
|
||||
promotionsTable.add(
|
||||
ImageGetter.getImage("OtherIcons/Star").apply {
|
||||
color = if (GUI.isAllowedChangeState() && unit.currentMovement > 0f && unit.attacksThisTurn == 0)
|
||||
Color.GOLDENROD
|
||||
else Color.GOLDENROD.darken(0.25f)
|
||||
}
|
||||
).size(24f).padLeft(8f)
|
||||
else null
|
||||
promotionsTable.onClick {
|
||||
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)
|
||||
|
@ -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.math.Vector2
|
||||
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.Image
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Label
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.GUI
|
||||
import com.unciv.logic.map.mapunit.MapUnit
|
||||
import com.unciv.models.TutorialTrigger
|
||||
import com.unciv.models.UncivSound
|
||||
import com.unciv.models.ruleset.unit.Promotion
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.components.BorderedTable
|
||||
import com.unciv.ui.components.extensions.colorFromRGB
|
||||
import com.unciv.ui.components.extensions.darken
|
||||
import com.unciv.ui.audio.SoundPlayer
|
||||
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.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.screens.basescreen.BaseScreen
|
||||
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
|
||||
|
||||
class PromotionNode(val promotion: Promotion) {
|
||||
var maxDepth = 0
|
||||
|
||||
/** How many level this promotion has */
|
||||
var levels = 1
|
||||
|
||||
val successors: ArrayList<PromotionNode> = ArrayList()
|
||||
val predecessors: ArrayList<PromotionNode> = ArrayList()
|
||||
|
||||
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)
|
||||
class PromotionPickerScreen(
|
||||
val unit: MapUnit,
|
||||
private val onChange: (() -> Unit)? = null
|
||||
) : PickerScreen(), RecreateOnResize {
|
||||
// Style stuff
|
||||
private val colors = skin[PromotionScreenColors::class.java]
|
||||
private val promotedLabelStyle = Label.LabelStyle(skin[Label.LabelStyle::class.java]).apply {
|
||||
fontColor = colors.promotedText
|
||||
}
|
||||
private val buttonCellMaxWidth: Float
|
||||
private val buttonCellMinWidth: Float
|
||||
|
||||
// Widgets
|
||||
private val promotionsTable = Table()
|
||||
private val promotionToButton = LinkedHashMap<String, PromotionButton>()
|
||||
private var selectedPromotion: PromotionButton? = null
|
||||
@ -150,138 +49,88 @@ class PromotionPickerScreen(val unit: MapUnit) : PickerScreen(), RecreateOnResiz
|
||||
private val canPromoteNow = canChangeState && canBePromoted &&
|
||||
unit.currentMovement > 0 && unit.attacksThisTurn == 0
|
||||
|
||||
// Logic
|
||||
private val tree = PromotionTree(unit)
|
||||
|
||||
|
||||
init {
|
||||
setDefaultCloseAction()
|
||||
|
||||
if (canPromoteNow) {
|
||||
rightSideButton.setText("Pick promotion".tr())
|
||||
rightSideButton.onClick(UncivSound.Promote) {
|
||||
if (selectedPromotion?.isPickable == true)
|
||||
acceptPromotion(selectedPromotion?.node)
|
||||
rightSideButton.onClick(UncivSound.Silent) {
|
||||
acceptPromotion(selectedPromotion)
|
||||
}
|
||||
} else {
|
||||
rightSideButton.isVisible = false
|
||||
}
|
||||
|
||||
descriptionLabel.setText(updateDescriptionLabel())
|
||||
updateDescriptionLabel()
|
||||
|
||||
val availablePromotionsGroup = Table()
|
||||
availablePromotionsGroup.defaults().pad(5f)
|
||||
|
||||
val unitType = unit.type
|
||||
val promotionsForUnitType = unit.civ.gameInfo.ruleset.unitPromotions.values.filter {
|
||||
it.unitTypes.contains(unitType.name) || unit.promotions.promotions.contains(it.name)
|
||||
}
|
||||
//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))
|
||||
if (canChangeState) {
|
||||
//Always allow the user to rename the unit as many times as they like.
|
||||
val renameButton = "Choose name for [${unit.name}]".toTextButton()
|
||||
renameButton.onClick {
|
||||
UnitRenamePopup(this, unit) {
|
||||
game.replaceCurrentScreen(recreate())
|
||||
}
|
||||
)
|
||||
}
|
||||
topTable.add(renameButton).pad(5f).row()
|
||||
}
|
||||
availablePromotionsGroup.add(renameButton)
|
||||
|
||||
topTable.add(availablePromotionsGroup).row()
|
||||
fillTable(promotionsForUnitType)
|
||||
// Create all buttons without placing them yet, measure
|
||||
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)
|
||||
}
|
||||
|
||||
private fun acceptPromotion(node: PromotionNode?) {
|
||||
private fun acceptPromotion(button: PromotionButton?) {
|
||||
// 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)
|
||||
game.replaceCurrentScreen(recreate())
|
||||
}
|
||||
|
||||
private fun fillTable(promotions: Collection<Promotion>) {
|
||||
val map = LinkedHashMap<String, PromotionNode>()
|
||||
|
||||
val availablePromotions = unit.promotions.getAvailablePromotions()
|
||||
|
||||
// Create nodes
|
||||
// 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
|
||||
// Can't use stage.addAction as the screen is going to die immediately
|
||||
val path = tree.getPathTo(button.node.promotion)
|
||||
if (path.size == 1) {
|
||||
Concurrency.runOnGLThread { SoundPlayer.play(UncivSound.Promote) }
|
||||
} else {
|
||||
Concurrency.runOnGLThread {
|
||||
SoundPlayer.play(UncivSound.Promote)
|
||||
Concurrency.run {
|
||||
delay(200)
|
||||
Concurrency.runOnGLThread { SoundPlayer.play(UncivSound.Promote) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse each root node tree and calculate max possible depths of each node
|
||||
for (node in map.values) {
|
||||
if (node.isRoot())
|
||||
node.calculateDepth(arrayListOf(node), 0)
|
||||
}
|
||||
for (promotion in path)
|
||||
unit.promotions.addPromotion(promotion.name)
|
||||
|
||||
// For each non-root node remove all predecessors except the one with the least max depth.
|
||||
// This is needed to compactify trees and remove circular dependencies (A -> B -> C -> A)
|
||||
for (node in map.values) {
|
||||
if (node.isRoot())
|
||||
continue
|
||||
onChange?.invoke()
|
||||
game.replaceCurrentScreen(recreate())
|
||||
}
|
||||
|
||||
// Choose best predecessor - the one with less depth
|
||||
var best: PromotionNode? = null
|
||||
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))
|
||||
}
|
||||
private fun fillTable() {
|
||||
val placedButtons = mutableSetOf<String>()
|
||||
|
||||
// Create cell matrix
|
||||
val maxColumns = map.size + 1
|
||||
val maxRows = map.size + 1
|
||||
|
||||
val cellMatrix = ArrayList<ArrayList<Cell<Actor>>>()
|
||||
for (y in 0..maxRows) {
|
||||
cellMatrix.add(ArrayList())
|
||||
for (x in 0..maxColumns) {
|
||||
val cell = promotionsTable.add()
|
||||
cellMatrix[y].add(cell)
|
||||
val maxColumns = tree.getMaxColumns()
|
||||
val maxRows = tree.getMaxRows()
|
||||
val cellMatrix = Array(maxRows + 1) {
|
||||
Array(maxColumns + 1) {
|
||||
promotionsTable.add() as Cell<Actor?>
|
||||
}.also {
|
||||
promotionsTable.row()
|
||||
}
|
||||
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 {
|
||||
for (i in 0 until levels) {
|
||||
if (cellMatrix[row][col+i].actor != null)
|
||||
@ -290,24 +139,17 @@ class PromotionPickerScreen(val unit: MapUnit) : PickerScreen(), RecreateOnResiz
|
||||
return true
|
||||
}
|
||||
|
||||
/** Recursively place buttons for node and it's successors into free cells */
|
||||
fun placeButton(col: Int, row: Int, node: PromotionNode) : Int {
|
||||
/** Recursively place buttons for node and its successors into free cells */
|
||||
fun placeButton(col: Int, row: Int, node: PromotionTree.PromotionNode) : Int {
|
||||
val name = node.promotion.name
|
||||
// If promotion button not yet placed
|
||||
if (promotionToButton[name] == null) {
|
||||
if (name !in placedButtons) {
|
||||
// If place is free - we place button
|
||||
if (isTherePlace(row, col, node.levels)) {
|
||||
val cell = cellMatrix[row][col]
|
||||
val isPromotionAvailable = node.promotion in availablePromotions
|
||||
val hasPromotion = unit.promotions.promotions.contains(name)
|
||||
val isPickable = canPromoteNow && isPromotionAvailable && !hasPromotion
|
||||
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)
|
||||
cellMatrix[row][col].setActor(promotionToButton[name])
|
||||
.pad(5f).padRight(20f)
|
||||
.minWidth(buttonCellMinWidth).maxWidth(buttonCellMaxWidth)
|
||||
placedButtons += name
|
||||
}
|
||||
// If place is not free - try to find another in the next row
|
||||
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.
|
||||
// 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 node.successors.filter {
|
||||
!promotionToButton.containsKey(it.promotion.name)
|
||||
}.map {
|
||||
placeButton(col+1, row, it)
|
||||
}.maxOfOrNull { it }?: row
|
||||
return node.children
|
||||
.filter { it.promotion.name !in placedButtons }
|
||||
.sortedBy { it.baseName != node.baseName } // Prioritize getting groups in a row - relying on sensible json "column" isn't enough
|
||||
.maxOfOrNull { placeButton(col + 1, row, it) }
|
||||
?: row
|
||||
}
|
||||
|
||||
// Build each tree starting from root nodes
|
||||
var row = 0
|
||||
for (node in map.values) {
|
||||
if (node.isRoot()) {
|
||||
row = placeButton(0, row, node)
|
||||
// Each root tree should start from a completely empty row.
|
||||
row += 1
|
||||
}
|
||||
for (node in tree.allRoots()) {
|
||||
row = placeButton(0, row, node)
|
||||
// Each root tree should start from a completely empty row.
|
||||
row += 1
|
||||
}
|
||||
|
||||
topTable.add(promotionsTable)
|
||||
|
||||
addConnectingLines()
|
||||
|
||||
addConnectingLines(emptySet())
|
||||
}
|
||||
|
||||
private fun getButton(allPromotions: Collection<Promotion>, node: PromotionNode,
|
||||
isPickable: Boolean = true, isPromoted: Boolean = false) : PromotionButton {
|
||||
private fun getButton(tree: PromotionTree, node: PromotionTree.PromotionNode) : PromotionButton {
|
||||
val isPickable = (!node.pathIsAmbiguous || node.distanceToAdopted == 1) && tree.canBuyUpTo(node.promotion)
|
||||
|
||||
val button = PromotionButton(
|
||||
node = node,
|
||||
isPromoted = isPromoted,
|
||||
isPickable = isPickable
|
||||
)
|
||||
val button = PromotionButton(node, isPickable, promotedLabelStyle, buttonCellMaxWidth - 60f)
|
||||
|
||||
button.onClick {
|
||||
selectedPromotion?.isSelected = false
|
||||
selectedPromotion?.updateColor()
|
||||
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)
|
||||
btn.updateColor()
|
||||
button.node.promotion.prerequisites.forEach { promotionToButton[it]?.apply {
|
||||
if (!this.isPromoted)
|
||||
bgColor = Prerequisite }}
|
||||
btn.updateColor(btn == selectedPromotion, pathAsSet, prerequisites)
|
||||
|
||||
rightSideButton.isEnabled = isPickable
|
||||
rightSideButton.setText(node.promotion.name.tr())
|
||||
descriptionLabel.setText(updateDescriptionLabel(node.promotion.getDescription(allPromotions)))
|
||||
updateDescriptionLabel(isPickable, tree, node, path)
|
||||
|
||||
addConnectingLines()
|
||||
addConnectingLines(pathAsSet)
|
||||
}
|
||||
|
||||
if (isPickable)
|
||||
button.onDoubleClick(UncivSound.Promote) {
|
||||
acceptPromotion(node)
|
||||
button.onDoubleClick(UncivSound.Silent) {
|
||||
acceptPromotion(button)
|
||||
}
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
private fun addConnectingLines() {
|
||||
private fun addConnectingLines(path: Set<Promotion>) {
|
||||
promotionsTable.pack()
|
||||
scrollPane.updateVisualScroll()
|
||||
|
||||
for (line in lines) line.remove()
|
||||
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 (prerequisite in button.node.promotion.prerequisites) {
|
||||
val currentNode = button.node
|
||||
for (prerequisite in currentNode.promotion.prerequisites) {
|
||||
val prerequisiteButton = promotionToButton[prerequisite] ?: continue
|
||||
val prerequisiteNode = prerequisiteButton.node
|
||||
|
||||
var buttonCoords = Vector2(0f, button.height / 2)
|
||||
button.localToStageCoordinates(buttonCoords)
|
||||
@ -397,15 +242,16 @@ class PromotionPickerScreen(val unit: MapUnit) : PickerScreen(), RecreateOnResiz
|
||||
prerequisiteButton.localToStageCoordinates(prerequisiteCoords)
|
||||
promotionsTable.stageToLocalCoordinates(prerequisiteCoords)
|
||||
|
||||
val isNodeInPath = currentNode.promotion in path
|
||||
val isSelectionPath = isNodeInPath &&
|
||||
(prerequisiteNode.isAdopted || prerequisiteNode.promotion in path)
|
||||
val lineColor = when {
|
||||
button.isSelected -> Selected
|
||||
prerequisiteButton.node.baseName == button.node.baseName -> Color.WHITE.cpy()
|
||||
else -> Color.CLEAR
|
||||
}
|
||||
val lineSize = when {
|
||||
button.isSelected -> 4f
|
||||
else -> 2f
|
||||
isSelectionPath -> colors.selected
|
||||
isNodeInPath -> colors.pathToSelection
|
||||
prerequisiteNode.baseName == currentNode.baseName -> colors.groupLines
|
||||
else -> colors.otherLines
|
||||
}
|
||||
val lineSize = if (isSelectionPath) 4f else 2f
|
||||
|
||||
if (buttonCoords.x < prerequisiteCoords.x) {
|
||||
val temp = buttonCoords.cpy()
|
||||
@ -413,67 +259,48 @@ class PromotionPickerScreen(val unit: MapUnit) : PickerScreen(), RecreateOnResiz
|
||||
prerequisiteCoords = temp
|
||||
}
|
||||
|
||||
|
||||
val halfLineSize = lineSize / 2
|
||||
if (buttonCoords.y != prerequisiteCoords.y) {
|
||||
|
||||
val deltaX = buttonCoords.x - prerequisiteCoords.x
|
||||
val deltaY = buttonCoords.y - prerequisiteCoords.y
|
||||
val halfLength = deltaX / 2f
|
||||
|
||||
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)
|
||||
val halfLength = deltaX / 2f + halfLineSize
|
||||
|
||||
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 {
|
||||
|
||||
val line = ImageGetter.getWhiteDot().apply {
|
||||
width = buttonCoords.x - prerequisiteCoords.x
|
||||
height = lineSize
|
||||
x = prerequisiteCoords.x
|
||||
y = prerequisiteCoords.y - lineSize / 2
|
||||
}
|
||||
line.color = lineColor
|
||||
promotionsTable.addActor(line)
|
||||
line.toBack()
|
||||
lines.add(line)
|
||||
|
||||
addLine(
|
||||
width = buttonCoords.x - prerequisiteCoords.x,
|
||||
height = lineSize,
|
||||
x = prerequisiteCoords.x,
|
||||
y = prerequisiteCoords.y - halfLineSize,
|
||||
color = lineColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (line in lines) {
|
||||
if (line.color == Selected)
|
||||
if (line.color == colors.selected || line.color == colors.pathToSelection)
|
||||
line.zIndex = lines.size
|
||||
}
|
||||
}
|
||||
@ -484,18 +311,29 @@ class PromotionPickerScreen(val unit: MapUnit) : PickerScreen(), RecreateOnResiz
|
||||
scrollPane.updateVisualScroll()
|
||||
}
|
||||
|
||||
private fun updateDescriptionLabel(): String {
|
||||
return unit.displayName().tr()
|
||||
private fun updateDescriptionLabel() {
|
||||
descriptionLabel.setText(unit.displayName().tr())
|
||||
}
|
||||
|
||||
private fun updateDescriptionLabel(promotionDescription: String): String {
|
||||
var newDescriptionText = unit.displayName().tr()
|
||||
newDescriptionText += "\n" + promotionDescription
|
||||
return newDescriptionText
|
||||
private fun updateDescriptionLabel(
|
||||
isPickable: Boolean,
|
||||
tree: PromotionTree,
|
||||
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 {
|
||||
val newScreen = PromotionPickerScreen(unit)
|
||||
val newScreen = PromotionPickerScreen(unit, onChange)
|
||||
newScreen.setScrollY(scrollPane.scrollY)
|
||||
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:
|
||||
|
||||
| Attribute | Type | Optional | Notes |
|
||||
| --------- | ---- | -------- | ----- |
|
||||
| name | String | Required | See above for "I, II, III" progressions |
|
||||
| prerequisites | List | Default empty | Prerequisite promotions |
|
||||
| effect | String | Default empty | Deprecated, use uniques instead |
|
||||
| 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/Unique-parameters.md#unit-uniques) |
|
||||
| civilopediaText | List | Default empty | see [civilopediaText chapter](Miscellaneous-JSON-files.md#civilopedia-text) |
|
||||
| Attribute | Type | Optional | Notes |
|
||||
|-----------------|--------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| name | String | Required | See above for "I, II, III" progressions |
|
||||
| prerequisites | List | Default empty | Prerequisite promotions |
|
||||
| effect | String | Default empty | Deprecated and ignored, use uniques instead |
|
||||
| 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. (…) |
|
||||
| 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. |
|
||||
| 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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user