City state election rigging (#11577)

* Added city-state Elections

* Added Elections notifications

* Removed temporary rigging elections in Spy.kt

* Modified votes from influence a little

* Fixed rigging election turns

* Fixed elections

* Randomised city-state election days

* Refactored geCapital

* Refactored election random to use randomWeighted

* Fixed getSkillModifier being private

* Updated translations and fixed a notification for election rigging
This commit is contained in:
Oskar Niesen
2024-05-14 02:58:07 -05:00
committed by GitHub
parent 5b002acfee
commit eb79a777a2
6 changed files with 70 additions and 39 deletions

View File

@ -1773,9 +1773,10 @@ Your spy [spyName] stole the Technology [techName] from [cityName]! =
Your spy [spyName] was killed trying to steal Technology in [cityName]! = Your spy [spyName] was killed trying to steal Technology in [cityName]! =
# Rigging elections # Rigging elections
A spy from [civName] tried to rig elections and was found and killed in [cityName] by [spyName]! =
Your spy [spyName] was killed trying to rig the election in [cityName]! =
Your spy successfully rigged the election in [cityName]! = Your spy successfully rigged the election in [cityName]! =
Your spy lost the election in [cityStateName] to [civName]! =
The election in [cityStateName] were rigged by [civName]! =
Your spy lost the election in [cityName]! =
# 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. =

View File

@ -29,7 +29,7 @@ class CityEspionageManager : IsPartOfGameInfoSerialization {
return civInfo.espionageManager.spyList.any { it.getCityOrNull() == city } return civInfo.espionageManager.spyList.any { it.getCityOrNull() == city }
} }
private fun getAllStationedSpies(): List<Spy> { fun getAllStationedSpies(): List<Spy> {
return city.civ.gameInfo.civilizations.flatMap { it.espionageManager.getSpiesInCity(city) } return city.civ.gameInfo.civilizations.flatMap { it.espionageManager.getSpiesInCity(city) }
} }

View File

@ -303,6 +303,7 @@ class Civilization : IsPartOfGameInfoSerialization {
toReturn.hasMovedAutomatedUnits = hasMovedAutomatedUnits toReturn.hasMovedAutomatedUnits = hasMovedAutomatedUnits
toReturn.statsHistory = statsHistory.clone() toReturn.statsHistory = statsHistory.clone()
toReturn.resourceStockpiles = resourceStockpiles.clone() toReturn.resourceStockpiles = resourceStockpiles.clone()
toReturn.cityStateTurnsUntilElection = cityStateTurnsUntilElection
return toReturn return toReturn
} }
@ -363,7 +364,7 @@ class Civilization : IsPartOfGameInfoSerialization {
var cityStatePersonality: CityStatePersonality = CityStatePersonality.Neutral var cityStatePersonality: CityStatePersonality = CityStatePersonality.Neutral
var cityStateResource: String? = null var cityStateResource: String? = null
var cityStateUniqueUnit: String? = null // Unique unit for militaristic city state. Might still be null if there are no appropriate units var cityStateUniqueUnit: String? = null // Unique unit for militaristic city state. Might still be null if there are no appropriate units
var cityStateTurnsUntilElection: Int = 0
fun hasMetCivTerritory(otherCiv: Civilization): Boolean = fun hasMetCivTerritory(otherCiv: Civilization): Boolean =
otherCiv.getCivTerritory().any { gameInfo.tileMap[it].isExplored(this) } otherCiv.getCivTerritory().any { gameInfo.tileMap[it].isExplored(this) }

View File

@ -14,6 +14,8 @@ import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.PlayerType import com.unciv.logic.civilization.PlayerType
import com.unciv.logic.civilization.PopupAlert import com.unciv.logic.civilization.PopupAlert
import com.unciv.logic.civilization.Proximity import com.unciv.logic.civilization.Proximity
import com.unciv.models.Spy
import com.unciv.models.SpyAction
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.nation.CityStateType import com.unciv.models.ruleset.nation.CityStateType
import com.unciv.models.ruleset.tile.ResourceSupplyList import com.unciv.models.ruleset.tile.ResourceSupplyList
@ -22,6 +24,7 @@ import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.stats.Stat import com.unciv.models.stats.Stat
import com.unciv.ui.components.extensions.randomWeighted
import com.unciv.ui.screens.victoryscreen.RankingType import com.unciv.ui.screens.victoryscreen.RankingType
import kotlin.math.min import kotlin.math.min
import kotlin.math.pow import kotlin.math.pow
@ -60,11 +63,63 @@ class CityStateFunctions(val civInfo: Civilization) {
civInfo.cityStateUniqueUnit = possibleUnits.random().name civInfo.cityStateUniqueUnit = possibleUnits.random().name
} }
// Set turns to elections to a random number so not every city-state has the same election date
civInfo.cityStateTurnsUntilElection = Random.nextInt(15)
// TODO: Return false if attempting to put a religious city-state in a game without religion // TODO: Return false if attempting to put a religious city-state in a game without religion
return true return true
} }
fun nextTurnElections() {
civInfo.cityStateTurnsUntilElection--
val capital = civInfo.getCapital()
if (civInfo.cityStateTurnsUntilElection <= 0) {
if (capital == null) return
civInfo.cityStateTurnsUntilElection = 15
val spies= capital.espionage.getAllStationedSpies().filter { it.action == SpyAction.RiggingElections }
if (spies.isEmpty()) return
fun getVotesFromSpy(spy: Spy?): Float {
if (spy == null) return 20f
var votes = (civInfo.getDiplomacyManager(spy.civInfo).influence / 2)
votes += (spy.getSkillModifier() * spy.getEfficiencyModifier()).toFloat() // ranges from 30 to 90
return votes
}
val parties: MutableList<Spy?> = spies.toMutableList()
parties.add(null) // Null spy is a neuteral party in the election
val randomSeed = capital.location.x * capital.location.y + 123f * civInfo.gameInfo.turns
val winner: Civilization? = parties.randomWeighted(Random(randomSeed.toInt())) { getVotesFromSpy(it) }?.civInfo
// There may be no winner, in that case all spies will loose 5 influence
if (winner != null) {
val allyCiv = civInfo.getAllyCiv()?.let { civInfo.gameInfo.getCivilization(it) }
// Winning civ gets influence and all others loose influence
for (civ in civInfo.getKnownCivs().toList()) {
val influence = if (civ == winner) 20f else -5f
civInfo.getDiplomacyManager(civ).addInfluence(influence)
if (civ == winner) {
civ.addNotification("Your spy successfully rigged the election in [${civInfo.civName}]!", capital.location, NotificationCategory.Espionage, NotificationIcon.Spy)
} else if (spies.any { it.civInfo == civ}) {
civ.addNotification("Your spy lost the election in [${civInfo.civName}] to [${winner.civName}]!", capital.location, NotificationCategory.Espionage, NotificationIcon.Spy)
} else if (civ == allyCiv) {
// If the previous ally has no spy in the city then we should notify them
allyCiv.addNotification("The election in [${civInfo.civName}] were rigged by [${winner.civName}]!", capital.location, NotificationCategory.Espionage, NotificationIcon.Spy)
}
}
} else {
// No spy won the election, the civs that tried to rig the election loose influence
for (spy in spies) {
civInfo.getDiplomacyManager(spy.civInfo).addInfluence(-5f)
spy.civInfo.addNotification("Your spy lost the election in [$capital]!", capital.location, NotificationCategory.Espionage, NotificationIcon.Spy)
}
}
}
}
fun turnsForGreatPersonFromCityState(): Int = ((37 + Random.Default.nextInt(7)) * civInfo.gameInfo.speed.modifier).toInt() fun turnsForGreatPersonFromCityState(): Int = ((37 + Random.Default.nextInt(7)) * civInfo.gameInfo.speed.modifier).toInt()
/** Gain a random great person from the city state */ /** Gain a random great person from the city state */

View File

@ -259,8 +259,10 @@ class TurnManager(val civInfo: Civilization) {
civInfo.policies.endTurn(nextTurnStats.culture.toInt()) civInfo.policies.endTurn(nextTurnStats.culture.toInt())
civInfo.totalCultureForContests += nextTurnStats.culture.toInt() civInfo.totalCultureForContests += nextTurnStats.culture.toInt()
if (civInfo.isCityState()) if (civInfo.isCityState()) {
civInfo.questManager.endTurn() civInfo.questManager.endTurn()
civInfo.cityStateFunctions.nextTurnElections()
}
// disband units until there are none left OR the gold values are normal // disband units until there are none left OR the gold values are normal
if (!civInfo.isBarbarian() && civInfo.gold <= -200 && nextTurnStats.gold.toInt() < 0) { if (!civInfo.isBarbarian() && civInfo.gold <= -200 && nextTurnStats.gold.toInt() < 0) {

View File

@ -22,7 +22,7 @@ enum class SpyAction(val displayString: String, val hasTurns: Boolean, internal
EstablishNetwork("Establishing Network", true, false, true), EstablishNetwork("Establishing Network", true, false, true),
Surveillance("Observing City", false, true), Surveillance("Observing City", false, true),
StealingTech("Stealing Tech", false, true, true), StealingTech("Stealing Tech", false, true, true),
RiggingElections("Rigging Elections", true, 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)
}, },
CounterIntelligence("Conducting Counter-intelligence", false, true) { CounterIntelligence("Conducting Counter-intelligence", false, true) {
@ -100,7 +100,7 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
SpyAction.EstablishNetwork -> { SpyAction.EstablishNetwork -> {
val city = getCity() // This should never throw an exception, as going to the hideout sets your action to None. val city = getCity() // This should never throw an exception, as going to the hideout sets your action to None.
if (city.civ.isCityState()) if (city.civ.isCityState())
setAction(SpyAction.RiggingElections, 10) setAction(SpyAction.RiggingElections, getCity().civ.cityStateTurnsUntilElection - 1)
else if (city.civ == civInfo) else if (city.civ == civInfo)
setAction(SpyAction.CounterIntelligence, 10) setAction(SpyAction.CounterIntelligence, 10)
else else
@ -131,7 +131,9 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
} }
} }
SpyAction.RiggingElections -> { SpyAction.RiggingElections -> {
rigElection() // No action done here
// Handled in CityStateFunctions.nextTurnElections()
turnsRemainingForAction = getCity().civ.cityStateTurnsUntilElection - 1
} }
SpyAction.Dead -> { SpyAction.Dead -> {
val oldSpyName = name val oldSpyName = name
@ -202,36 +204,6 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
} }
} }
private fun rigElection() {
val city = getCity()
val cityStateCiv = city.civ
// TODO: Simple implementation, please implement this in the future. This is a guess.
turnsRemainingForAction = 10
if (cityStateCiv.getAllyCiv() != null && cityStateCiv.getAllyCiv() != civInfo.civName) {
val allyCiv = civInfo.gameInfo.getCivilization(cityStateCiv.getAllyCiv()!!)
val defendingSpy = allyCiv.espionageManager.getSpyAssignedToCity(city)
if (defendingSpy != null) {
var spyResult = Random(randomSeed()).nextInt(120)
spyResult -= getSkillModifier()
spyResult += defendingSpy.getSkillModifier()
if (spyResult > 100) {
// The Spy was killed (use the notification without EspionageAction)
allyCiv.addNotification("A spy from [${civInfo.civName}] tried to rig elections and was found and killed in [${city}] by [${defendingSpy.name}]!",
city.location, NotificationCategory.Espionage, NotificationIcon.Spy)
addNotification("Your spy [$name] was killed trying to rig the election in [$city]!")
killSpy()
defendingSpy.levelUpSpy()
return
}
}
}
// Starts at 10 influence and increases by 3 for each extra rank.
cityStateCiv.getDiplomacyManager(civInfo).addInfluence(7f + rank * 3)
civInfo.addNotification("Your spy successfully rigged the election in [$city]!", city.location,
NotificationCategory.Espionage, NotificationIcon.Spy)
}
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
@ -285,7 +257,7 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
* Or - chance range of best result is 0% (rank 1 vs rank 3 defender) to 30% (rank 3 vs no defender), range of worst is 53% to 3%, respectively. * Or - chance range of best result is 0% (rank 1 vs rank 3 defender) to 30% (rank 3 vs no defender), range of worst is 53% to 3%, respectively.
*/ */
// Todo Moddable as some global and/or in-game-gainable Uniques? // Todo Moddable as some global and/or in-game-gainable Uniques?
private fun getSkillModifier() = rank * 30 fun getSkillModifier() = rank * 30
/** /**
* Gets a friendly and enemy efficiency uniques for the spy at the location * Gets a friendly and enemy efficiency uniques for the spy at the location