mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-24 03:53:12 -04: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:
parent
d4cfd4e563
commit
c7a7bf1474
@ -1778,6 +1778,15 @@ Your spy lost the election in [cityStateName] to [civName]! =
|
|||||||
The election in [cityStateName] were rigged by [civName]! =
|
The election in [cityStateName] were rigged by [civName]! =
|
||||||
Your spy lost the election in [cityName]! =
|
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
|
# 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 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. =
|
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
|
package com.unciv.logic.automation.unit
|
||||||
|
|
||||||
import com.unciv.logic.civilization.Civilization
|
import com.unciv.logic.civilization.Civilization
|
||||||
|
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
|
||||||
import com.unciv.models.Spy
|
import com.unciv.models.Spy
|
||||||
import com.unciv.models.SpyAction
|
import com.unciv.models.SpyAction
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
@ -48,6 +49,7 @@ class EspionageAutomation(val civInfo: Civilization) {
|
|||||||
val randomCity = civInfo.gameInfo.getCities().filter { spy.canMoveTo(it) }.toList().randomOrNull()
|
val randomCity = civInfo.gameInfo.getCities().filter { spy.canMoveTo(it) }.toList().randomOrNull()
|
||||||
spy.moveTo(randomCity)
|
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())
|
spy.moveTo(civInfo.cities.filter { spy.canMoveTo(it) }.randomOrNull())
|
||||||
return spy.action == SpyAction.CounterIntelligence
|
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)
|
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) {
|
fun setInfluence(amount: Float) {
|
||||||
influence = max(amount, MINIMUM_INFLUENCE)
|
influence = max(amount, MINIMUM_INFLUENCE)
|
||||||
civInfo.cityStateFunctions.updateAllyCivForCityState()
|
civInfo.cityStateFunctions.updateAllyCivForCityState()
|
||||||
|
@ -25,6 +25,7 @@ enum class SpyAction(val displayString: String, val hasTurns: Boolean, internal
|
|||||||
RiggingElections("Rigging Elections", false, true) {
|
RiggingElections("Rigging Elections", false, true) {
|
||||||
override fun isDoingWork(spy: Spy) = !spy.civInfo.isAtWarWith(spy.getCity().civ)
|
override fun isDoingWork(spy: Spy) = !spy.civInfo.isAtWarWith(spy.getCity().civ)
|
||||||
},
|
},
|
||||||
|
Coup("Coup", true, true, true),
|
||||||
CounterIntelligence("Counter-intelligence", false, true) {
|
CounterIntelligence("Counter-intelligence", false, true) {
|
||||||
override fun isDoingWork(spy: Spy) = spy.turnsRemainingForAction > 0
|
override fun isDoingWork(spy: Spy) = spy.turnsRemainingForAction > 0
|
||||||
},
|
},
|
||||||
@ -79,7 +80,7 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
|
|||||||
this.espionageManager = civInfo.espionageManager
|
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.
|
assert(!newAction.hasTurns || turns > 0) // hasTurns==false but turns > 0 is allowed (CounterIntelligence), hasTurns==true and turns==0 is not.
|
||||||
action = newAction
|
action = newAction
|
||||||
turnsRemainingForAction = turns
|
turnsRemainingForAction = turns
|
||||||
@ -135,6 +136,9 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
|
|||||||
// Handled in CityStateFunctions.nextTurnElections()
|
// Handled in CityStateFunctions.nextTurnElections()
|
||||||
turnsRemainingForAction = getCity().civ.cityStateTurnsUntilElection - 1
|
turnsRemainingForAction = getCity().civ.cityStateTurnsUntilElection - 1
|
||||||
}
|
}
|
||||||
|
SpyAction.Coup -> {
|
||||||
|
initiateCoup()
|
||||||
|
}
|
||||||
SpyAction.Dead -> {
|
SpyAction.Dead -> {
|
||||||
val oldSpyName = name
|
val oldSpyName = name
|
||||||
name = espionageManager.getSpyName()
|
name = espionageManager.getSpyName()
|
||||||
@ -203,7 +207,92 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
|
|||||||
otherCiv.getDiplomacyManager(civInfo).addModifier(DiplomaticModifiers.SpiedOnUs, -15f)
|
otherCiv.getDiplomacyManager(civInfo).addModifier(DiplomaticModifiers.SpiedOnUs, -15f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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?) {
|
fun moveTo(city: City?) {
|
||||||
if (city == null) { // Moving to spy hideout
|
if (city == null) { // Moving to spy hideout
|
||||||
location = null
|
location = null
|
||||||
|
@ -10,6 +10,7 @@ import com.unciv.UncivGame
|
|||||||
import com.unciv.logic.city.City
|
import com.unciv.logic.city.City
|
||||||
import com.unciv.logic.civilization.Civilization
|
import com.unciv.logic.civilization.Civilization
|
||||||
import com.unciv.models.Spy
|
import com.unciv.models.Spy
|
||||||
|
import com.unciv.models.SpyAction
|
||||||
import com.unciv.models.translations.tr
|
import com.unciv.models.translations.tr
|
||||||
import com.unciv.ui.components.SmallButtonStyle
|
import com.unciv.ui.components.SmallButtonStyle
|
||||||
import com.unciv.ui.components.extensions.addSeparatorVertical
|
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.input.onClick
|
||||||
import com.unciv.ui.components.widgets.AutoScrollPane
|
import com.unciv.ui.components.widgets.AutoScrollPane
|
||||||
import com.unciv.ui.images.ImageGetter
|
import com.unciv.ui.images.ImageGetter
|
||||||
|
import com.unciv.ui.popups.ConfirmPopup
|
||||||
import com.unciv.ui.screens.pickerscreens.PickerScreen
|
import com.unciv.ui.screens.pickerscreens.PickerScreen
|
||||||
import com.unciv.ui.screens.worldscreen.WorldScreen
|
import com.unciv.ui.screens.worldscreen.WorldScreen
|
||||||
|
|
||||||
@ -41,7 +43,7 @@ class EspionageOverviewScreen(val civInfo: Civilization, val worldScreen: WorldS
|
|||||||
private var selectedSpy: Spy? = null
|
private var selectedSpy: Spy? = null
|
||||||
|
|
||||||
// if the value == null, this means the Spy Hideout.
|
// 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>()
|
private var moveSpyButtons = hashMapOf<Spy, TextButton>()
|
||||||
|
|
||||||
/** Readability shortcut */
|
/** Readability shortcut */
|
||||||
@ -101,7 +103,7 @@ class EspionageOverviewScreen(val civInfo: Civilization, val worldScreen: WorldS
|
|||||||
|
|
||||||
private fun updateCityList() {
|
private fun updateCityList() {
|
||||||
citySelectionTable.clear()
|
citySelectionTable.clear()
|
||||||
moveSpyHereButtons.clear()
|
spyActionButtons.clear()
|
||||||
citySelectionTable.add()
|
citySelectionTable.add()
|
||||||
citySelectionTable.add("City".toLabel()).padTop(10f)
|
citySelectionTable.add("City".toLabel()).padTop(10f)
|
||||||
citySelectionTable.add("Spy present".toLabel()).padTop(10f).row()
|
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(label).fill()
|
||||||
citySelectionTable.add(getSpyIcons(manager.getSpiesInCity(city)))
|
citySelectionTable.add(getSpyIcons(manager.getSpiesInCity(city)))
|
||||||
|
|
||||||
val moveSpyHereButton = MoveToCityButton(city)
|
val spy = civInfo.espionageManager.getSpyAssignedToCity(city)
|
||||||
citySelectionTable.add(moveSpyHereButton)
|
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()
|
citySelectionTable.row()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,8 +187,12 @@ class EspionageOverviewScreen(val civInfo: Civilization, val worldScreen: WorldS
|
|||||||
add(getSpyIcon(spy))
|
add(getSpyIcon(spy))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private abstract inner class SpyCityActionButton : Button(SmallButtonStyle()) {
|
||||||
|
open fun setDirection(align: Int) { }
|
||||||
|
}
|
||||||
|
|
||||||
// city == null is interpreted as 'spy hideout'
|
// 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)
|
val arrow = ImageGetter.getArrowImage(Align.left)
|
||||||
init {
|
init {
|
||||||
arrow.setSize(24f)
|
arrow.setSize(24f)
|
||||||
@ -192,11 +204,11 @@ class EspionageOverviewScreen(val civInfo: Civilization, val worldScreen: WorldS
|
|||||||
resetSelection()
|
resetSelection()
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
moveSpyHereButtons[this] = city
|
spyActionButtons[this] = city
|
||||||
isVisible = false
|
isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setDirection(align: Int) {
|
override fun setDirection(align: Int) {
|
||||||
arrow.rotation = if (align == Align.right) 0f else 180f
|
arrow.rotation = if (align == Align.right) 0f else 180f
|
||||||
isDisabled = align == Align.right
|
isDisabled = align == Align.right
|
||||||
}
|
}
|
||||||
@ -211,7 +223,7 @@ class EspionageOverviewScreen(val civInfo: Civilization, val worldScreen: WorldS
|
|||||||
selectedSpyButton = moveSpyButton
|
selectedSpyButton = moveSpyButton
|
||||||
selectedSpy = spy
|
selectedSpy = spy
|
||||||
selectedSpyButton!!.label.setText(Constants.cancel.tr())
|
selectedSpyButton!!.label.setText(Constants.cancel.tr())
|
||||||
for ((button, city) in moveSpyHereButtons) {
|
for ((button, city) in spyActionButtons) {
|
||||||
if (city == spy.getCityOrNull()) {
|
if (city == spy.getCityOrNull()) {
|
||||||
button.isVisible = true
|
button.isVisible = true
|
||||||
button.setDirection(Align.right)
|
button.setDirection(Align.right)
|
||||||
@ -227,7 +239,37 @@ class EspionageOverviewScreen(val civInfo: Civilization, val worldScreen: WorldS
|
|||||||
selectedSpy = null
|
selectedSpy = null
|
||||||
selectedSpyButton?.label?.setText("Move".tr())
|
selectedSpyButton?.label?.setText("Move".tr())
|
||||||
selectedSpyButton = null
|
selectedSpyButton = null
|
||||||
for ((button, _) in moveSpyHereButtons)
|
for ((button, _) in spyActionButtons)
|
||||||
button.isVisible = false
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user