Little Diplomatic Victory makeover (#9756)

* Linting and give two votes to UN owner

* Allow human player to abstain, show UN 2 votes

* More info on voting results

* AI won't vote for hated enemies

* Improve PopupAlert handling

* Translation templates

* One missing template
This commit is contained in:
SomeTroglodyte 2023-07-10 14:25:59 +02:00 committed by GitHub
parent 8f761642f6
commit a737747284
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 201 additions and 77 deletions

View File

@ -1426,11 +1426,20 @@ Worst = Schlechtester
Turns until the next\ndiplomacy victory vote: [amount] = Runden bis zur nächsten Abstimmung\nüber den Diplomatiesieg: [amount]
Choose a civ to vote for = Wähle eine Zivilisation, für die du abstimmen möchtest
Choose who should become the world leader and win a Diplomatic Victory! = Wähle, wer der Anführer der Welt werden und somit den Diplomatiesieg erhalten soll
Voted for = Abgestimmt für
Voted for = hat gestimmt für
Vote for [civilizationName] = Abstimmen für [civilizationName]
Abstain = Enthalten
Continue = Fortfahren
Abstained = Enthalten
Vote for World Leader = Stimme für den Anführer der Welt ab
[number] votes = [number] Stimmen
[number] vote = [number] Stimme
No valid votes were cast. = Es wurden keine gültigen Stimmen abgegeben.
Minimum votes for electing a world leader: [number] = Ein Anführer der Welt benötigt mindestens [number] Stimmen, um gewählt zu werden
Tied in first position: [civNames] = Gleichstand auf dem ersten Platz: [civNames]
No world leader was elected. = Es wurde kein Anführer der Welt gewählt.
You have been elected world leader! = Du wurdest zum Anführer der Welt gewählt!
[leaderName] of [civ] has been elected world leader! = [leaderName] von [civ] wurde zum Anführer der Welt gewählt!
Replay = Wiederholung
# Capturing a city
@ -6477,4 +6486,3 @@ However, it will reflect the mods you are playing! The combination of base rules
If you opened the Civilopedia from the main menu, the "Ruleset" will be that of the last game you started. = Wenn du die Zivilopädie vom Hauptmenü öffnest, wird das "Regelwerk" des letzten gestarteten Spiels ausschlaggebend sein.
Letters can select categories, and when there are multiple categories matching the same letter, you can press that repeatedly to cycle between these. = Buchstaben können Kategorien auswählen. Wenn mehrere Kategorien mit dem gleichen Buchstaben anfangen, kannst du den Buchstaben wiederholt drücken, um durch diese durchzuwechseln. (Aktuell werden die Buchstaben aus der englischen Version verwendet.)
The arrow keys allow navigation as well - left/right for categories, up/down for entries. = Die Pfeiltasten können auch zur Navigation verwendet werden. - Links/rechts für Kategorien, hoch/runter für Einträge.

View File

@ -1426,11 +1426,20 @@ Worst =
Turns until the next\ndiplomacy victory vote: [amount] =
Choose a civ to vote for =
Choose who should become the world leader and win a Diplomatic Victory! =
Voted for =
Vote for [civilizationName] =
Vote for World Leader =
Abstain =
Continue =
Abstained =
Vote for World Leader =
Voted for =
[number] votes =
[number] vote =
No valid votes were cast. =
Minimum votes for electing a world leader: [number] =
Tied in first position: [civNames] =
No world leader was elected. =
You have been elected world leader! =
[leaderName] of [civ] has been elected world leader! =
Replay =
# Capturing a city

View File

@ -1,6 +1,7 @@
package com.unciv.logic
import com.unciv.Constants
import com.unciv.GUI
import com.unciv.UncivGame
import com.unciv.UncivGame.Version
import com.unciv.json.json
@ -35,6 +36,7 @@ import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.Speed
import com.unciv.models.ruleset.nation.Difficulty
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.tr
import com.unciv.ui.audio.MusicMood
import com.unciv.ui.audio.MusicTrackChooserFlags
import com.unciv.ui.screens.pickerscreens.Github.repoNameToFolderName
@ -109,8 +111,8 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
var victoryData:VictoryData? = null
// Maps a civ to the civ they voted for
var diplomaticVictoryVotesCast = HashMap<String, String>()
/** Maps a civ to the civ they voted for - `null` on the value side means they abstained */
var diplomaticVictoryVotesCast = HashMap<String, String?>()
// Set to false whenever the results still need te be processed
var diplomaticVictoryVotesProcessed = false
@ -228,6 +230,35 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
fun getAliveCityStates() = civilizations.filter { it.isAlive() && it.isCityState() }
fun getAliveMajorCivs() = civilizations.filter { it.isAlive() && it.isMajorCiv() }
/** Gets civilizations in their commonly used order - City-states last,
* otherwise alphabetically by culture and translation. [civToSortFirst] can be used to force
* a specific Civilization to be listed first.
*
* Barbarians and Spectators always excluded, other filter criteria are [includeCityStates],
* [includeDefeated] and optionally an [additionalFilter].
*/
fun getCivsSorted(
includeCityStates: Boolean = true,
includeDefeated: Boolean = false,
civToSortFirst: Civilization? = null,
additionalFilter: ((Civilization) -> Boolean)? = null
): Sequence<Civilization> {
val collator = GUI.getSettings().getCollatorFromLocale()
return civilizations.asSequence()
.filterNot {
it.isBarbarian() ||
it.isSpectator() ||
!includeDefeated && it.isDefeated() ||
!includeCityStates && it.isCityState() ||
additionalFilter?.invoke(it) == false
}
.sortedWith(
compareBy<Civilization> { it != civToSortFirst }
.thenByDescending { it.isMajorCiv() }
.thenBy(collator) { it.civName.tr(hideIcons = true) }
)
}
/** Returns the first spectator for a [playerId] or creates one if none found */
fun getSpectator(playerId: String): Civilization {
val gameSpectatorCiv = civilizations.firstOrNull {

View File

@ -13,6 +13,8 @@ class BarbarianAutomation(val civInfo: Civilization) {
civInfo.units.getCivUnits().filter { it.baseUnit.isRanged() }.forEach { automateUnit(it) }
civInfo.units.getCivUnits().filter { it.baseUnit.isMelee() }.forEach { automateUnit(it) }
civInfo.units.getCivUnits().filter { !it.baseUnit.isRanged() && !it.baseUnit.isMelee() }.forEach { automateUnit(it) }
// fix buildup of alerts - to shrink saves and ease debugging
civInfo.popupAlerts.clear()
}
private fun automateUnit(unit: MapUnit) {

View File

@ -48,6 +48,7 @@ import com.unciv.ui.screens.victoryscreen.RankingType
import java.util.SortedMap
import java.util.TreeMap
import kotlin.math.min
import kotlin.random.Random
object NextTurnAutomation {
@ -1077,24 +1078,29 @@ object NextTurnAutomation {
// Technically, this function should also check for civs that have liberated one or more cities
// However, that can be added in another update, this PR is large enough as it is.
private fun tryVoteForDiplomaticVictory(civInfo: Civilization) {
if (!civInfo.mayVoteForDiplomaticVictory()) return
val chosenCiv: String? = if (civInfo.isMajorCiv()) {
private fun tryVoteForDiplomaticVictory(civ: Civilization) {
if (!civ.mayVoteForDiplomaticVictory()) return
val knownMajorCivs = civInfo.getKnownCivs().filter { it.isMajorCiv() }
val chosenCiv: String? = if (civ.isMajorCiv()) {
val knownMajorCivs = civ.getKnownCivs().filter { it.isMajorCiv() }
val highestOpinion = knownMajorCivs
.maxOfOrNull {
civInfo.getDiplomacyManager(it).opinionOfOtherCiv()
civ.getDiplomacyManager(it).opinionOfOtherCiv()
}
if (highestOpinion == null) null
else knownMajorCivs.filter { civInfo.getDiplomacyManager(it).opinionOfOtherCiv() == highestOpinion}.toList().random().civName
if (highestOpinion == null) null // Abstain if we know nobody
else if (highestOpinion < -80 || highestOpinion < -40 && highestOpinion + Random.Default.nextInt(40) < -40)
null // Abstain if we hate everybody (proportional chance in the RelationshipLevel.Enemy range - lesser evil)
else knownMajorCivs
.filter { civ.getDiplomacyManager(it).opinionOfOtherCiv() == highestOpinion }
.toList().random().civName
} else {
civInfo.getAllyCiv()
civ.getAllyCiv()
}
civInfo.diplomaticVoteForCiv(chosenCiv)
civ.diplomaticVoteForCiv(chosenCiv)
}
private fun issueRequests(civInfo: Civilization) {

View File

@ -703,7 +703,7 @@ class Civilization : IsPartOfGameInfoSerialization {
&& gameInfo.civilizations.any { it.isMajorCiv() && !it.isDefeated() && it != this }
fun diplomaticVoteForCiv(chosenCivName: String?) {
if (chosenCivName != null) gameInfo.diplomaticVictoryVotesCast[civName] = chosenCivName
gameInfo.diplomaticVictoryVotesCast[civName] = chosenCivName
}
fun shouldShowDiplomaticVotingResults() =

View File

@ -17,19 +17,9 @@ class DiplomacyFunctions(val civInfo: Civilization){
/** A sorted Sequence of all other civs we know (excluding barbarians and spectators) */
fun getKnownCivsSorted(includeCityStates: Boolean = true, includeDefeated: Boolean = false) =
civInfo.gameInfo.civilizations.asSequence()
.filterNot {
it == civInfo ||
it.isBarbarian() ||
it.isSpectator() ||
!civInfo.knows(it) ||
!includeDefeated && it.isDefeated() ||
!includeCityStates && it.isCityState()
}
.sortedWith(
compareByDescending<Civilization> { it.isMajorCiv() }
.thenBy (UncivGame.Current.settings.getCollatorFromLocale()) { it.civName.tr(hideIcons = true) }
)
civInfo.gameInfo.getCivsSorted(includeCityStates, includeDefeated) {
it != civInfo && civInfo.knows(it)
}
fun makeCivilizationsMeet(otherCiv: Civilization, warOnContact: Boolean = false) {

View File

@ -307,8 +307,13 @@ class TurnManager(val civInfo: Civilization) {
civInfo.gameInfo.victoryData =
VictoryData(civInfo.civName, victoryType, civInfo.gameInfo.turns)
for (civInfo in civInfo.gameInfo.civilizations)
civInfo.popupAlerts.add(PopupAlert(AlertType.GameHasBeenWon, civInfo.civName))
// Notify other human players about this civInfo's victory
for (otherCiv in civInfo.gameInfo.civilizations) {
// Skip winner, displaying VictoryScreen is handled separately in WorldScreen.update
// by checking `viewingCiv.isDefeated() || gameInfo.checkForVictory()`
if (otherCiv.playerType != PlayerType.Human || otherCiv == civInfo) continue
otherCiv.popupAlerts.add(PopupAlert(AlertType.GameHasBeenWon, ""))
}
}
}

View File

@ -6,7 +6,9 @@ import com.unciv.logic.civilization.Civilization
import com.unciv.models.Counter
import com.unciv.models.ruleset.Milestone
import com.unciv.models.ruleset.Victory
import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.tr
class VictoryManager : IsPartOfGameInfoSerialization {
@Transient
@ -24,14 +26,29 @@ class VictoryManager : IsPartOfGameInfoSerialization {
return toReturn
}
private fun calculateDiplomaticVotingResults(votesCast: HashMap<String, String>): Counter<String> {
private fun calculateDiplomaticVotingResults(votesCast: HashMap<String, String?>): Counter<String> {
val results = Counter<String>()
for (castVote in votesCast) {
results.add(castVote.value, 1)
// UN Owner gets 2 votes in G&K
val (_, civOwningUN) = getUNBuildingAndOwnerNames()
for ((voter, votedFor) in votesCast) {
if (votedFor == null) continue // null means Abstained
results.add(votedFor, if (voter == civOwningUN) 2 else 1)
}
return results
}
/** Finds the Building and Owner of the United Nations (or whatever the Mod called it)
* - if it's built at all and only if the owner is alive
* @return `first`: Building name, `second`: Owner civ name; both null if not found
*/
fun getUNBuildingAndOwnerNames(): Pair<String?, String?> = civInfo.gameInfo.civilizations.asSequence()
.filterNot { it.isBarbarian() || it.isSpectator() || it.isDefeated() }
.flatMap { civ -> civ.cities.asSequence()
.flatMap { it.cityConstructions.getBuiltBuildings() }
.filter { it.hasUnique(UniqueType.OneTimeTriggerVoting, stateForConditionals = StateForConditionals.IgnoreConditionals) }
.map { it.name to civ.civName }
}.firstOrNull() ?: (null to null)
private fun votesNeededForDiplomaticVictory(): Int {
val civCount = civInfo.gameInfo.civilizations.count { !it.isDefeated() }
@ -56,6 +73,29 @@ class VictoryManager : IsPartOfGameInfoSerialization {
return (results.none { it != bestCiv && it.value == bestCiv.value })
}
fun getDiplomaticVictoryVoteBreakdown(): String {
val results = calculateDiplomaticVotingResults(civInfo.gameInfo.diplomaticVictoryVotesCast)
val (voteCount, winnerList) = results.asSequence()
.groupBy({ it.value }, { it.key }).asSequence()
.sortedByDescending { it.key } // key is vote count here
.firstOrNull()
?: return "No valid votes were cast."
val lines = arrayListOf<String>()
val minVotes = votesNeededForDiplomaticVictory()
if (voteCount < minVotes)
lines += "Minimum votes for electing a world leader: [$minVotes]"
if (winnerList.size > 1)
lines += "Tied in first position: [${winnerList.joinToString { it.tr() }}]" // Yes with icons
val winnerCiv = civInfo.gameInfo.getCivilization(winnerList.first())
lines += when {
lines.isNotEmpty() -> "No world leader was elected."
winnerCiv == civInfo -> "You have been elected world leader!"
else -> "${civInfo.nation.getLeaderDisplayName()} has been elected world leader!"
}
return lines.joinToString("\n") { "{$it}" }
}
fun getVictoryTypeAchieved(): String? {
if (!civInfo.isMajorCiv()) return null
val enabledVictories = civInfo.gameInfo.gameParameters.victoryTypes

View File

@ -1,11 +1,13 @@
package com.unciv.ui.screens.pickerscreens
import com.badlogic.gdx.scenes.scene2d.Actor
import com.unciv.UncivGame
import com.unciv.logic.civilization.Civilization
import com.unciv.models.UncivSound
import com.unciv.models.translations.tr
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.input.onDoubleClick
import com.unciv.ui.images.ImageGetter
class DiplomaticVotePickerScreen(private val votingCiv: Civilization) : PickerScreen() {
private var chosenCiv: String? = null
@ -16,27 +18,39 @@ class DiplomaticVotePickerScreen(private val votingCiv: Civilization) : PickerSc
descriptionLabel.setText("Choose who should become the world leader and win a Diplomatic Victory!".tr())
val choosableCivs = votingCiv.gameInfo.civilizations.filter { it.isMajorCiv() && it != votingCiv && !it.isDefeated() }
for (civ in choosableCivs)
{
val button = PickerPane.getPickerOptionButton(
val choosableCivs = votingCiv.diplomacyFunctions.getKnownCivsSorted(false)
for (civ in choosableCivs) {
addButton(civ.civName, "Vote for [${civ.civName}]", civ.civName,
ImageGetter.getNationPortrait(
civ.nation,
PickerPane.pickerOptionIconSize
), civ.civName
)
)
button.pack()
button.onClick {
chosenCiv = civ.civName
pick("Vote for [${civ.civName}]".tr())
}
addButton("Abstain", "Abstain", null,
ImageGetter.getImage("OtherIcons/Stop").apply {
setSize(PickerPane.pickerOptionIconSize, PickerPane.pickerOptionIconSize)
}
topTable.add(button).pad(10f).row()
}
)
rightSideButton.onClick(UncivSound.Chimes) {
votingCiv.diplomaticVoteForCiv(chosenCiv!!)
UncivGame.Current.popScreen()
}
rightSideButton.onClick(UncivSound.Chimes, ::voteAndClose)
}
private fun voteAndClose() {
votingCiv.diplomaticVoteForCiv(chosenCiv)
UncivGame.Current.popScreen()
}
private fun addButton(caption: String, pickText: String, choice: String?, icon: Actor) {
val button = PickerPane.getPickerOptionButton(icon, caption)
button.onClick {
chosenCiv = choice
pick(pickText.tr())
}
button.onDoubleClick(UncivSound.Chimes) {
chosenCiv = choice
voteAndClose()
}
topTable.add(button).fillX().pad(10f).row()
}
}

View File

@ -1,55 +1,74 @@
package com.unciv.ui.screens.pickerscreens
import com.unciv.UncivGame
import com.badlogic.gdx.utils.Align
import com.unciv.logic.civilization.CivFlags
import com.unciv.logic.civilization.Civilization
import com.unciv.models.UncivSound
import com.unciv.models.translations.tr
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.components.extensions.enable
import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.input.KeyCharAndCode
import com.unciv.ui.components.input.keyShortcuts
import com.unciv.ui.components.input.onActivation
import com.unciv.ui.images.ImageGetter
class DiplomaticVoteResultScreen(val votesCast: HashMap<String, String>, val viewingCiv: Civilization) : PickerScreen() {
class DiplomaticVoteResultScreen(
private val votesCast: HashMap<String, String?>,
viewingCiv: Civilization
) : PickerScreen() {
val gameInfo = viewingCiv.gameInfo
private val constructionNameUN: String?
private val civOwningUN: String?
init {
closeButton.remove()
addVote(viewingCiv.civName)
val findUN = viewingCiv.victoryManager.getUNBuildingAndOwnerNames()
constructionNameUN = findUN.first
civOwningUN = findUN.second
for (civ in gameInfo.civilizations.filter { it.isMajorCiv() && it != viewingCiv })
addVote(civ.civName)
for (civ in gameInfo.civilizations.filter { it.isCityState() })
addVote(civ.civName)
val orderedCivs = gameInfo.getCivsSorted(civToSortFirst = viewingCiv)
for (civ in orderedCivs) addVote(civ)
rightSideButton.onClick(UncivSound.Click) {
val result = viewingCiv.victoryManager.getDiplomaticVictoryVoteBreakdown()
descriptionLabel.setAlignment(Align.center)
descriptionLabel.setText(result.tr())
rightSideButton.onActivation(UncivSound.Click) {
viewingCiv.addFlag(CivFlags.ShowDiplomaticVotingResults.name, -1)
UncivGame.Current.popScreen()
game.popScreen()
}
rightSideButton.keyShortcuts.add(KeyCharAndCode.BACK)
rightSideButton.keyShortcuts.add(KeyCharAndCode.SPACE)
rightSideButton.enable()
rightSideButton.setText("Continue".tr())
bottomTable.cells[0].minWidth(rightSideButton.prefWidth + 20f) // center descriptionLabel
}
private fun addVote(civName: String) {
val civ = gameInfo.civilizations.firstOrNull { it.civName == civName }
if (civ == null || civ.isDefeated()) return
private fun addVote(civ: Civilization) {
val civName = civ.civName
topTable.add(ImageGetter.getNationPortrait(civ.nation, 30f)).pad(10f)
topTable.add(civName.toLabel()).pad(20f)
if (civName !in votesCast.keys) {
topTable.add("Abstained".toLabel()).row()
return
topTable.add(civName.toLabel(hideIcons = true)).pad(20f)
if (civName == civOwningUN && constructionNameUN != null) {
topTable.add(ImageGetter.getConstructionPortrait(constructionNameUN, 30f))
.pad(10f)
topTable.add("[2] votes".toLabel())
} else {
topTable.add("[1] vote".toLabel()).colspan(2)
}
val votedCiv = gameInfo.civilizations.firstOrNull { it.civName == votesCast[civName] }!!
if (votedCiv.isDefeated()) {
topTable.add("Abstained".toLabel()).row()
return
}
fun abstained() = topTable.add("Abstained".toLabel()).colspan(3).row()
val votedCivName = votesCast[civName]
?: return abstained()
topTable.add("Voted for".toLabel()).pad(20f)
val votedCiv = gameInfo.getCivilization(votedCivName)
if (votedCiv.isDefeated()) return abstained()
topTable.add("Voted for".toLabel()).pad(20f).padRight(0f)
topTable.add(ImageGetter.getNationPortrait(votedCiv.nation, 30f)).pad(10f)
topTable.add(votedCiv.civName.toLabel()).row()
topTable.add(votedCiv.civName.toLabel(hideIcons = true))
topTable.row()
}
}

View File

@ -14,12 +14,12 @@ import com.unciv.models.ruleset.Victory
import com.unciv.models.translations.tr
import com.unciv.ui.audio.MusicMood
import com.unciv.ui.audio.MusicTrackChooserFlags
import com.unciv.ui.components.input.KeyCharAndCode
import com.unciv.ui.components.TabbedPager
import com.unciv.ui.components.extensions.areSecretKeysPressed
import com.unciv.ui.components.extensions.enable
import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.input.KeyCharAndCode
import com.unciv.ui.components.input.onClick
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.basescreen.RecreateOnResize