(UI) Politics overview diagram: Add legend popup (#10451)

* Refactor Battle damage shadowed Label into reusable WidgetGroup

* Make Politics Overview "Grid header" fixed and some linting

* Add a Legend popup explaining line colors in the Politics Overview Diagram: Click inside

* Missing translation template

* Final decision on background didn't make it in properly

* Remove unused png - those are now in the Construction atlas

* Smaller background image

* Color and shadow choices

* Redo atlas to include both 10505 and this
This commit is contained in:
SomeTroglodyte
2023-12-22 08:37:47 +01:00
committed by GitHub
parent c08270362e
commit d3ac2c5ede
11 changed files with 412 additions and 251 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -0,0 +1,7 @@
Used in GlobalPoliticsOverviewTable.DiagramLegendPopup as "easter egg" background.
Uses 256x256 pixels in its atlas - at the time of adding it, there was ample leftover room,
but once the main atlas fill up, please move to another or even its own little atlas,
and maybe drop this file.
Will be seldom used!

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 532 KiB

After

Width:  |  Height:  |  Size: 534 KiB

View File

@ -1424,6 +1424,7 @@ Number of your cities\ndemanding this resource for\n'We Love The King Day' =
Politics =
Show global politics =
Show diagram =
Diagram line colors =
At war with [enemy] =
Defensive pact with [civName] =
Friends with [civName] =

View File

@ -19,7 +19,8 @@ import kotlin.math.max
import kotlin.math.sign
enum class RelationshipLevel(val color: Color) {
// War is tested separately for the Diplomacy Screen. Colored RED.
// DiplomaticStatus.War is tested separately for the Diplomacy Screen. Colored RED.
// DiplomaticStatus.DefensivePact - similar. Colored CYAN.
Unforgivable(Color.FIREBRICK),
Enemy(Color.YELLOW),
Afraid(Color(0x5300ffff)), // HSV(260,100,100)

View File

@ -0,0 +1,59 @@
package com.unciv.ui.components.widgets
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.scenes.scene2d.ui.Stack
import com.badlogic.gdx.utils.Align
import com.unciv.Constants
import com.unciv.models.translations.tr
import com.unciv.ui.components.extensions.toLabel
/**
* A widget containing two [Label]s superimposed with an offset to create a shadow effect.
*
* Reported [prefWidth], [prefHeight], [minWidth] and [minHeight] are always those of the Label plus `shadowOffset`
*
* @param text The label text (sic), autotranslated
* @param fontSize as the name says
* @param labelColor as the name says
* @param shadowColor as the name says
* @param hideIcons passed to [translation function][String.tr]
* @param shadowOffset displacement distance of the shadow to right and to bottom
*/
class ShadowedLabel(
text: String,
fontSize: Int = Constants.defaultFontSize,
labelColor: Color = Color.WHITE,
shadowColor: Color = Color.BLACK,
hideIcons: Boolean = true,
shadowOffset: Float = 1f
) : Stack() {
private val widthWithShadow: Float
private val heightWithShadow: Float
init {
touchable = Touchable.disabled
val shadow = text.toLabel(shadowColor, fontSize, Align.bottomRight, hideIcons)
shadow.touchable = Touchable.disabled
addActor(shadow)
val label = text.toLabel(labelColor, fontSize, Align.topLeft, hideIcons)
label.touchable = Touchable.disabled
addActor(label)
shadow.zIndex = 0
// Displace the shadow under the label by shadowOffset.
// Extending our size is enough due to their different Align values.
widthWithShadow = label.prefWidth + shadowOffset
heightWithShadow = label.prefHeight + shadowOffset
setSize(width + shadowOffset, height + shadowOffset)
}
override fun getPrefWidth() = widthWithShadow
override fun getPrefHeight() = heightWithShadow
// A Label has min=pref, but Stack overrides so we must override too
// or Stack will return the smaller values directly from Label
override fun getMinWidth() = widthWithShadow
override fun getMinHeight() = heightWithShadow
}

View File

@ -1,8 +1,10 @@
package com.unciv.ui.screens.overviewscreen
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.Group
import com.badlogic.gdx.scenes.scene2d.Stage
import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
@ -15,24 +17,29 @@ import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
import com.unciv.logic.map.HexMath
import com.unciv.models.ruleset.Policy.PolicyBranchType
import com.unciv.models.ruleset.nation.getContrastRatio
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
import com.unciv.ui.components.extensions.addBorder
import com.unciv.ui.components.extensions.addSeparator
import com.unciv.ui.components.extensions.addSeparatorVertical
import com.unciv.ui.components.extensions.center
import com.unciv.ui.components.extensions.equalizeColumns
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.components.fonts.Fonts
import com.unciv.ui.components.input.onActivation
import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.widgets.AutoScrollPane
import com.unciv.ui.components.widgets.ColorMarkupLabel
import com.unciv.ui.components.widgets.ShadowedLabel
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popups.AnimatedMenuPopup
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.diplomacyscreen.DiplomacyScreen
import kotlin.math.roundToInt
class GlobalPoliticsOverviewTable (
class GlobalPoliticsOverviewTable(
viewingPlayer: Civilization,
overviewScreen: EmpireOverviewScreen,
persistedData: EmpireOverviewTabPersistableData? = null
@ -66,17 +73,31 @@ class GlobalPoliticsOverviewTable (
init {
top()
if (persistableData.showDiagram) updateDiagram()
else updatePoliticsTable()
}
override fun getFixedContent() = fixedContent
//region Politics Table View
private fun updatePoliticsTable() {
persistableData.showDiagram = false
clear()
fixedContent.clear()
createGlobalPoliticsHeader()
createGlobalPoliticsTable()
equalizeColumns(fixedContent, this)
}
/** Clears fixedContent and adds the header cells.
* Needs to stay matched to [createGlobalPoliticsTable].
*
* 9 Columns: 5 info, 4 separators. First gets an empty header for contend below = civ image
*/
private fun createGlobalPoliticsHeader() = fixedContent.run {
val diagramButton = "Show diagram".toTextButton().onClick(::updateDiagram)
clear()
add()
addSeparatorVertical(Color.GRAY)
add("Civilization Info".toLabel())
@ -86,16 +107,22 @@ class GlobalPoliticsOverviewTable (
add("Wonders".toLabel())
addSeparatorVertical(Color.GRAY)
add(Table().apply {
add("Relations".toLabel()).row()
add("Relations".toLabel()).padTop(10f).row()
add(diagramButton).pad(10f)
})
createGlobalPoliticsTable()
addSeparator(Color.GRAY)
}
/** Clears [EmpireOverviewTab]'s main Table and adds data columns/rows.
* Needs to stay matched to [createGlobalPoliticsHeader].
*/
private fun createGlobalPoliticsTable() {
clear()
for (civ in sequenceOf(viewingPlayer) + viewingPlayer.diplomacyFunctions.getKnownCivsSorted(includeCityStates = false)) {
addSeparator(Color.GRAY)
// We already have a separator under the fixed header, we only need them here between rows.
// This is also the replacement for calling row() explicitly
if (cells.size > 0) addSeparator(Color.GRAY)
// civ image
add(ImageGetter.getNationPortrait(civ.nation, 100f)).pad(20f)
@ -225,7 +252,8 @@ class GlobalPoliticsOverviewTable (
return politicsTable
}
override fun getFixedContent() = fixedContent
//endregion
//region Diagram View ("Ball of Yarn")
// Refresh content and determine landscape/portrait layout
private fun updateDiagram() {
@ -406,6 +434,16 @@ class GlobalPoliticsOverviewTable (
init {
setSize(freeSize, freeSize)
// An Image Actor does not respect alpha for its hit area, it's always square, but we want a clickable _circle_
// Radius to show legend should be no larger than freeSize / 2.25f - 15f (see below), let's make it a little smaller
val clickableArea = ClickableCircle(freeSize / 1.25f - 25f)
clickableArea.onActivation {
DiagramLegendPopup(stage, this)
}
clickableArea.center(this)
addActor(clickableArea)
val civGroups = HashMap<String, Actor>()
val civLines = HashMap<String, MutableSet<Actor>>()
val civCount = undefeatedCivs.count()
@ -426,7 +464,7 @@ class GlobalPoliticsOverviewTable (
addActor(civGroup)
}
for (civ in undefeatedCivs)
for (civ in undefeatedCivs) {
for (diplomacy in civ.diplomacy.values) {
if (diplomacy.otherCiv() !in undefeatedCivs) continue
val civGroup = civGroups[civ.civName]!!
@ -437,7 +475,8 @@ class GlobalPoliticsOverviewTable (
startY = civGroup.y + civGroup.height / 2,
endX = otherCivGroup.x + otherCivGroup.width / 2,
endY = otherCivGroup.y + otherCivGroup.height / 2,
width = 2f)
width = 2f
)
statusLine.color = if (diplomacy.diplomaticStatus == DiplomaticStatus.War) Color.RED
else if (diplomacy.diplomaticStatus == DiplomaticStatus.DefensivePact
@ -451,6 +490,64 @@ class GlobalPoliticsOverviewTable (
addActorAt(0, statusLine)
}
}
}
}
private class DiagramLegendPopup(stage: Stage, diagram: Actor) : AnimatedMenuPopup(stage, diagram.getCenterInStageCoordinates()) {
init {
touchable = Touchable.enabled
onActivation { close() }
}
companion object {
private fun Actor.getCenterInStageCoordinates(): Vector2 = localToStageCoordinates(Vector2(width / 2, height / 2))
const val lineWidth = 3f // a little thicker than the actual diagram
const val lowContrastWidth = 4f
const val lineLength = 120f
}
override fun createContentTable(): Table {
val legend = Table()
legend.background = ImageGetter.getDrawable("OtherIcons/Politics-diagram-bg")
legend.add(ShadowedLabel("Diagram line colors", Constants.headingFontSize)).colspan(2).row()
//todo Rethink hardcoding together with the statusLine.color one in DiplomacyGroup
legend.addLegendRow("War", Color.RED)
for (level in RelationshipLevel.values()) {
val lineColor = if (level == RelationshipLevel.Ally) Color.CYAN else level.color
legend.addLegendRow(level.name, lineColor)
}
legend.addLegendRow(Constants.defensivePact, Color.CYAN)
return super.createContentTable()!!.apply {
add(legend).grow()
}
}
fun Table.addLegendRow(text: String, color: Color) {
// empiric hack to equalize the "visual impact" a little. Afraid is worst at contrast 1.4, Enemy has 9.8
val contrast = getContrastRatio(Color.DARK_GRAY, color).toFloat()
val width = lineWidth + (lowContrastWidth - lineWidth) / contrast.coerceAtLeast(1f)
val line = ImageGetter.getLine(0f, width / 2, lineLength, width / 2, width)
line.color = color
add(line).size(lineLength, width).padTop(5f)
add(ShadowedLabel(text)).padLeft(5f).padTop(10f).row()
}
}
/** An Image Actor does not respect alpha for its hit area, it's always square, but we want a clickable _circle_ */
private class ClickableCircle(size: Float) : Group() {
val center = Vector2(size / 2, size / 2)
val maxDst2 = size * size / 4 // squared radius
init {
touchable = Touchable.enabled
setSize(size, size)
}
override fun hit(x: Float, y: Float, touchable: Boolean): Actor? {
return if (center.dst2(x, y) < maxDst2) this else null
}
}
//endregion
}

View File

@ -5,14 +5,12 @@ import com.badlogic.gdx.math.Interpolation
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Group
import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.actions.Actions
import com.badlogic.gdx.scenes.scene2d.actions.FloatAction
import com.badlogic.gdx.scenes.scene2d.actions.RelativeTemporalAction
import com.badlogic.gdx.scenes.scene2d.actions.SequenceAction
import com.badlogic.gdx.scenes.scene2d.actions.TemporalAction
import com.badlogic.gdx.scenes.scene2d.ui.Image
import com.badlogic.gdx.scenes.scene2d.ui.Stack
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup
import com.badlogic.gdx.utils.Align
@ -20,8 +18,8 @@ import com.unciv.UncivGame
import com.unciv.logic.battle.ICombatant
import com.unciv.logic.battle.MapUnitCombatant
import com.unciv.logic.map.HexMath
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.tilegroups.TileSetStrings
import com.unciv.ui.components.widgets.ShadowedLabel
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.worldscreen.WorldScreen
@ -210,17 +208,7 @@ object BattleTableHelpers {
private fun createDamageLabel(damage: Int, target: Actor) {
if (damage == 0) return
val label = (-damage).toString().toLabel(Color.RED, damageLabelFontSize, Align.topLeft, true)
label.touchable = Touchable.disabled
val shadow = (-damage).toString().toLabel(Color.BLACK, damageLabelFontSize, Align.bottomRight, true)
shadow.touchable = Touchable.disabled
val container = Stack(shadow, label)
container.touchable = Touchable.disabled
container.pack()
// The +1f is what displaces the shadow under the red label
container.setSize(container.width + 1f, container.height + 1f)
val container = ShadowedLabel((-damage).toString(), damageLabelFontSize, Color.RED)
val targetRight = target.run { localToStageCoordinates(Vector2(width, height * 0.5f)) }
container.setPosition(targetRight.x, targetRight.y, Align.center)
target.stage.addActor(container)

View File

@ -734,6 +734,7 @@ Unless otherwise specified, all the following are from [the Noun Project](https:
- [charts](https://thenounproject.com/icon/charts-2312023/) by Srinivas Agra (gimped to appear bolder) for the Charts page
- [framed image](https://thenounproject.com/icon/framed-image-2332187/) by Jose Dean for Victory Illustrations page
- [down](https://thenounproject.com/icon/down-39378/) by Cengiz SARI for Show unit destination
- [Cat](https://thenounproject.com/icon/cat-158942/) by Josi for Politics overview diagram legend
### Main menu