mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-21 13:18:56 +07:00
City state coup (#11586)
* Added coup success calculation * Added a coup button * Added a coup button functionality * Improved coup chance calculation * Added coup effects * Fixed random value being too high * Fixed percent chance roll * Hid enemy spy factor from the chance text * Added coup notifications * Added translations * Updated a notification * Style changes * Put some text onto multiple lines * Fixed "failed staged" notification * Added missing translation * Finished fixing merge conflicts * Added AI to city-state coups * Coup notifications are now sent to the spectators as well * Changed spy rank modifier to be additive
This commit is contained in:
@ -1778,6 +1778,15 @@ Your spy lost the election in [cityStateName] to [civName]! =
|
||||
The election in [cityStateName] were rigged by [civName]! =
|
||||
Your spy lost the election in [cityName]! =
|
||||
|
||||
# City-Ctate Coups
|
||||
Stage Coup =
|
||||
Your spy [spyName] successfully staged a coup in [cityName]! =
|
||||
A spy from [civName] successfully staged a coup in our former ally [cityStateName]! =
|
||||
A spy from [civName] successfully staged a coup in [cityStateName]! =
|
||||
A spy from [civName] failed to stage a coup in our ally [cityStateName] and was killed! =
|
||||
Our spy [spyName] failed to stage a coup in [cityStateName] and was killed! =
|
||||
Do you want to stage a coup in [civName] with a [percent]% chance of success? =
|
||||
|
||||
# Spy fleeing city
|
||||
After the city of [cityName] was destroyed, your spy [spyName] has fled back to our hideout. =
|
||||
After the city of [cityName] was conquered, your spy [spyName] has fled back to our hideout. =
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.unciv.logic.automation.unit
|
||||
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
|
||||
import com.unciv.models.Spy
|
||||
import com.unciv.models.SpyAction
|
||||
import kotlin.random.Random
|
||||
@ -48,6 +49,7 @@ class EspionageAutomation(val civInfo: Civilization) {
|
||||
val randomCity = civInfo.gameInfo.getCities().filter { spy.canMoveTo(it) }.toList().randomOrNull()
|
||||
spy.moveTo(randomCity)
|
||||
}
|
||||
spies.forEach { checkIfShouldStageCoup(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -79,4 +81,16 @@ class EspionageAutomation(val civInfo: Civilization) {
|
||||
spy.moveTo(civInfo.cities.filter { spy.canMoveTo(it) }.randomOrNull())
|
||||
return spy.action == SpyAction.CounterIntelligence
|
||||
}
|
||||
|
||||
private fun checkIfShouldStageCoup(spy: Spy) {
|
||||
if (!spy.canDoCoup()) return
|
||||
if (spy.getCoupChanceOfSuccess(false) < .7) return
|
||||
val allyCiv = spy.getCity().civ.getAllyCiv()?.let { civInfo.gameInfo.getCivilization(it) }
|
||||
// Don't coup ally city-states
|
||||
if (allyCiv != null && civInfo.getDiplomacyManager(allyCiv).isRelationshipLevelGE(RelationshipLevel.Friend)) return
|
||||
val spies = civInfo.espionageManager.spyList
|
||||
val randomSeed = spies.size + spies.indexOf(spy) + civInfo.gameInfo.turns
|
||||
val randomAction = Random(randomSeed).nextInt(100)
|
||||
if (randomAction < 20) spy.setAction(SpyAction.Coup, 1)
|
||||
}
|
||||
}
|
||||
|
@ -323,6 +323,15 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
|
||||
setInfluence(influence + amount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces the influence to zero, or if they have negative influence does nothing
|
||||
* @param amount A positive value to subtract from the influecne
|
||||
*/
|
||||
fun reduceInfluence(amount: Float) {
|
||||
if (influence <= 0) return
|
||||
influence = (influence - amount).coerceAtLeast(0f)
|
||||
}
|
||||
|
||||
fun setInfluence(amount: Float) {
|
||||
influence = max(amount, MINIMUM_INFLUENCE)
|
||||
civInfo.cityStateFunctions.updateAllyCivForCityState()
|
||||
|
@ -25,6 +25,7 @@ enum class SpyAction(val displayString: String, val hasTurns: Boolean, internal
|
||||
RiggingElections("Rigging Elections", false, true) {
|
||||
override fun isDoingWork(spy: Spy) = !spy.civInfo.isAtWarWith(spy.getCity().civ)
|
||||
},
|
||||
Coup("Coup", true, true, true),
|
||||
CounterIntelligence("Counter-intelligence", false, true) {
|
||||
override fun isDoingWork(spy: Spy) = spy.turnsRemainingForAction > 0
|
||||
},
|
||||
@ -79,7 +80,7 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
|
||||
this.espionageManager = civInfo.espionageManager
|
||||
}
|
||||
|
||||
private fun setAction(newAction: SpyAction, turns: Int = 0) {
|
||||
fun setAction(newAction: SpyAction, turns: Int = 0) {
|
||||
assert(!newAction.hasTurns || turns > 0) // hasTurns==false but turns > 0 is allowed (CounterIntelligence), hasTurns==true and turns==0 is not.
|
||||
action = newAction
|
||||
turnsRemainingForAction = turns
|
||||
@ -135,6 +136,9 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
|
||||
// Handled in CityStateFunctions.nextTurnElections()
|
||||
turnsRemainingForAction = getCity().civ.cityStateTurnsUntilElection - 1
|
||||
}
|
||||
SpyAction.Coup -> {
|
||||
initiateCoup()
|
||||
}
|
||||
SpyAction.Dead -> {
|
||||
val oldSpyName = name
|
||||
name = espionageManager.getSpyName()
|
||||
@ -204,6 +208,91 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
|
||||
}
|
||||
}
|
||||
|
||||
fun canDoCoup(): Boolean = getCityOrNull() != null && getCity().civ.isCityState() && isSetUp() && getCity().civ.getAllyCiv() != civInfo.civName
|
||||
|
||||
/**
|
||||
* Initiates a coup if this spies civ is not the ally of the city-state.
|
||||
* The coup will only happen at the end of the Civ's turn for save scum reasons, so a play may not reload in multiplayer.
|
||||
* If successfull the coup will
|
||||
*/
|
||||
private fun initiateCoup() {
|
||||
if (!canDoCoup()) {
|
||||
// Maybe we are the new ally of the city-state
|
||||
// However we know that we are still in the city and it hasn't been conquered
|
||||
setAction(SpyAction.RiggingElections, 10)
|
||||
return
|
||||
}
|
||||
val successChance = getCoupChanceOfSuccess(true)
|
||||
val randomValue = Random(randomSeed()).nextFloat()
|
||||
if (randomValue <= successChance) {
|
||||
// Success
|
||||
val cityState = getCity().civ
|
||||
val pastAlly = cityState.getAllyCiv()?.let { civInfo.gameInfo.getCivilization(it) }
|
||||
val previousInfluence = if (pastAlly != null) cityState.getDiplomacyManager(pastAlly).getInfluence() else 80f
|
||||
cityState.getDiplomacyManager(civInfo).setInfluence(previousInfluence)
|
||||
|
||||
civInfo.addNotification("Your spy [$name] successfully staged a coup in [${cityState.civName}]!", getCity().location,
|
||||
NotificationCategory.Espionage, NotificationIcon.Spy, cityState.civName)
|
||||
if (pastAlly != null) {
|
||||
cityState.getDiplomacyManager(pastAlly).reduceInfluence(20f)
|
||||
pastAlly.addNotification("A spy from [${civInfo.civName}] successfully staged a coup in our former ally [${cityState.civName}]!", getCity().location,
|
||||
NotificationCategory.Espionage, civInfo.civName, NotificationIcon.Spy, cityState.civName)
|
||||
pastAlly.getDiplomacyManager(civInfo).addModifier(DiplomaticModifiers.SpiedOnUs, -15f)
|
||||
}
|
||||
for (civ in cityState.getKnownCivsWithSpectators()) {
|
||||
if (civ == pastAlly || civ == civInfo) continue
|
||||
civ.addNotification("A spy from [${civInfo.civName}] successfully staged a coup in [${cityState.civName}]!", getCity().location,
|
||||
NotificationCategory.Espionage, civInfo.civName, NotificationIcon.Spy, cityState.civName)
|
||||
if (civ.isSpectator()) continue
|
||||
cityState.getDiplomacyManager(civ).reduceInfluence(10f) // Guess
|
||||
}
|
||||
setAction(SpyAction.RiggingElections, 10)
|
||||
cityState.cityStateFunctions.updateAllyCivForCityState()
|
||||
|
||||
} else {
|
||||
// Failure
|
||||
val cityState = getCity().civ
|
||||
val allyCiv = cityState.getAllyCiv()?.let { civInfo.gameInfo.getCivilization(it) }
|
||||
val spy = allyCiv?.espionageManager?.getSpyAssignedToCity(getCity())
|
||||
cityState.getDiplomacyManager(civInfo).addInfluence(-20f)
|
||||
allyCiv?.addNotification("A spy from [${civInfo.civName}] failed to stag a coup in our ally [${cityState.civName}] and was killed!", getCity().location,
|
||||
NotificationCategory.Espionage, civInfo.civName, NotificationIcon.Spy, cityState.civName)
|
||||
allyCiv?.getDiplomacyManager(civInfo)?.addModifier(DiplomaticModifiers.SpiedOnUs, -10f)
|
||||
|
||||
civInfo.addNotification("Our spy [$name] failed to stag a coup in [${cityState.civName}] and was killed!", getCity().location,
|
||||
NotificationCategory.Espionage, civInfo.civName, NotificationIcon.Spy, cityState.civName)
|
||||
|
||||
killSpy()
|
||||
spy?.levelUpSpy() // Technically not in Civ V, but it's like the same thing as with counter-intelligence
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the success chance of a coup in this city state.
|
||||
*/
|
||||
fun getCoupChanceOfSuccess(includeUnkownFactors: Boolean): Float {
|
||||
val cityState = getCity().civ
|
||||
var successPercentage = 50f
|
||||
|
||||
// Influence difference should always be a positive value
|
||||
var influenceDifference: Float = if (cityState.getAllyCiv() != null)
|
||||
cityState.getDiplomacyManager(cityState.getAllyCiv()!!).getInfluence()
|
||||
else 60f
|
||||
influenceDifference -= cityState.getDiplomacyManager(civInfo).getInfluence()
|
||||
successPercentage -= influenceDifference / 2f
|
||||
|
||||
// If we are viewing the success chance we don't want to reveal that there is a defending spy
|
||||
val defendingSpy = if (includeUnkownFactors)
|
||||
cityState.getAllyCiv()?.let { civInfo.gameInfo.getCivilization(it) }?.espionageManager?.getSpyAssignedToCity(getCity())
|
||||
else null
|
||||
|
||||
val spyRanks = getSkillModifier() - (defendingSpy?.getSkillModifier() ?: 0)
|
||||
successPercentage += spyRanks / 2f // Each rank counts for 15%
|
||||
|
||||
successPercentage = successPercentage.coerceIn(0f, 85f)
|
||||
return successPercentage / 100f
|
||||
}
|
||||
|
||||
fun moveTo(city: City?) {
|
||||
if (city == null) { // Moving to spy hideout
|
||||
location = null
|
||||
|
@ -10,6 +10,7 @@ import com.unciv.UncivGame
|
||||
import com.unciv.logic.city.City
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.models.Spy
|
||||
import com.unciv.models.SpyAction
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.components.SmallButtonStyle
|
||||
import com.unciv.ui.components.extensions.addSeparatorVertical
|
||||
@ -24,6 +25,7 @@ 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.images.ImageGetter
|
||||
import com.unciv.ui.popups.ConfirmPopup
|
||||
import com.unciv.ui.screens.pickerscreens.PickerScreen
|
||||
import com.unciv.ui.screens.worldscreen.WorldScreen
|
||||
|
||||
@ -41,7 +43,7 @@ class EspionageOverviewScreen(val civInfo: Civilization, val worldScreen: WorldS
|
||||
private var selectedSpy: Spy? = null
|
||||
|
||||
// if the value == null, this means the Spy Hideout.
|
||||
private var moveSpyHereButtons = hashMapOf<MoveToCityButton, City?>()
|
||||
private var spyActionButtons = hashMapOf<SpyCityActionButton, City?>()
|
||||
private var moveSpyButtons = hashMapOf<Spy, TextButton>()
|
||||
|
||||
/** Readability shortcut */
|
||||
@ -101,7 +103,7 @@ class EspionageOverviewScreen(val civInfo: Civilization, val worldScreen: WorldS
|
||||
|
||||
private fun updateCityList() {
|
||||
citySelectionTable.clear()
|
||||
moveSpyHereButtons.clear()
|
||||
spyActionButtons.clear()
|
||||
citySelectionTable.add()
|
||||
citySelectionTable.add("City".toLabel()).padTop(10f)
|
||||
citySelectionTable.add("Spy present".toLabel()).padTop(10f).row()
|
||||
@ -145,8 +147,14 @@ class EspionageOverviewScreen(val civInfo: Civilization, val worldScreen: WorldS
|
||||
citySelectionTable.add(label).fill()
|
||||
citySelectionTable.add(getSpyIcons(manager.getSpiesInCity(city)))
|
||||
|
||||
val moveSpyHereButton = MoveToCityButton(city)
|
||||
citySelectionTable.add(moveSpyHereButton)
|
||||
val spy = civInfo.espionageManager.getSpyAssignedToCity(city)
|
||||
if (city.civ.isCityState() && spy != null && spy.canDoCoup()) {
|
||||
val coupButton = CoupButton(city, spy.action == SpyAction.Coup)
|
||||
citySelectionTable.add(coupButton)
|
||||
} else {
|
||||
val moveSpyHereButton = MoveToCityButton(city)
|
||||
citySelectionTable.add(moveSpyHereButton)
|
||||
}
|
||||
citySelectionTable.row()
|
||||
}
|
||||
|
||||
@ -179,8 +187,12 @@ class EspionageOverviewScreen(val civInfo: Civilization, val worldScreen: WorldS
|
||||
add(getSpyIcon(spy))
|
||||
}
|
||||
|
||||
private abstract inner class SpyCityActionButton : Button(SmallButtonStyle()) {
|
||||
open fun setDirection(align: Int) { }
|
||||
}
|
||||
|
||||
// city == null is interpreted as 'spy hideout'
|
||||
private inner class MoveToCityButton(city: City?) : Button(SmallButtonStyle()) {
|
||||
private inner class MoveToCityButton(city: City?) : SpyCityActionButton() {
|
||||
val arrow = ImageGetter.getArrowImage(Align.left)
|
||||
init {
|
||||
arrow.setSize(24f)
|
||||
@ -192,11 +204,11 @@ class EspionageOverviewScreen(val civInfo: Civilization, val worldScreen: WorldS
|
||||
resetSelection()
|
||||
update()
|
||||
}
|
||||
moveSpyHereButtons[this] = city
|
||||
spyActionButtons[this] = city
|
||||
isVisible = false
|
||||
}
|
||||
|
||||
fun setDirection(align: Int) {
|
||||
override fun setDirection(align: Int) {
|
||||
arrow.rotation = if (align == Align.right) 0f else 180f
|
||||
isDisabled = align == Align.right
|
||||
}
|
||||
@ -211,7 +223,7 @@ class EspionageOverviewScreen(val civInfo: Civilization, val worldScreen: WorldS
|
||||
selectedSpyButton = moveSpyButton
|
||||
selectedSpy = spy
|
||||
selectedSpyButton!!.label.setText(Constants.cancel.tr())
|
||||
for ((button, city) in moveSpyHereButtons) {
|
||||
for ((button, city) in spyActionButtons) {
|
||||
if (city == spy.getCityOrNull()) {
|
||||
button.isVisible = true
|
||||
button.setDirection(Align.right)
|
||||
@ -227,7 +239,37 @@ class EspionageOverviewScreen(val civInfo: Civilization, val worldScreen: WorldS
|
||||
selectedSpy = null
|
||||
selectedSpyButton?.label?.setText("Move".tr())
|
||||
selectedSpyButton = null
|
||||
for ((button, _) in moveSpyHereButtons)
|
||||
for ((button, _) in spyActionButtons)
|
||||
button.isVisible = false
|
||||
}
|
||||
|
||||
private inner class CoupButton(city: City, isCurrentAction: Boolean) : SpyCityActionButton() {
|
||||
val fist = ImageGetter.getStatIcon("Resistance")
|
||||
init {
|
||||
fist.setSize(24f)
|
||||
add(fist).size(24f)
|
||||
fist.setOrigin(Align.center)
|
||||
if (isCurrentAction) fist.color = Color.WHITE
|
||||
else fist.color = Color.DARK_GRAY
|
||||
onClick {
|
||||
val spy = selectedSpy!!
|
||||
if (!isCurrentAction) {
|
||||
ConfirmPopup(this@EspionageOverviewScreen,
|
||||
"Do you want to stage a coup in [${city.civ.civName}] with a " +
|
||||
"[${(selectedSpy!!.getCoupChanceOfSuccess(false) * 100f).toInt()}]% " +
|
||||
"chance of success?", "Stage Coup") {
|
||||
spy.setAction(SpyAction.Coup, 1)
|
||||
fist.color = Color.DARK_GRAY
|
||||
update()
|
||||
}.open()
|
||||
} else {
|
||||
spy.setAction(SpyAction.CounterIntelligence, 10)
|
||||
fist.color = Color.WHITE
|
||||
update()
|
||||
}
|
||||
}
|
||||
spyActionButtons[this] = city
|
||||
isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user