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]! =
# 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 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
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 }
}
private fun getAllStationedSpies(): List<Spy> {
fun getAllStationedSpies(): List<Spy> {
return city.civ.gameInfo.civilizations.flatMap { it.espionageManager.getSpiesInCity(city) }
}

View File

@ -303,6 +303,7 @@ class Civilization : IsPartOfGameInfoSerialization {
toReturn.hasMovedAutomatedUnits = hasMovedAutomatedUnits
toReturn.statsHistory = statsHistory.clone()
toReturn.resourceStockpiles = resourceStockpiles.clone()
toReturn.cityStateTurnsUntilElection = cityStateTurnsUntilElection
return toReturn
}
@ -363,7 +364,7 @@ class Civilization : IsPartOfGameInfoSerialization {
var cityStatePersonality: CityStatePersonality = CityStatePersonality.Neutral
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 cityStateTurnsUntilElection: Int = 0
fun hasMetCivTerritory(otherCiv: Civilization): Boolean =
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.PopupAlert
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.nation.CityStateType
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.unit.BaseUnit
import com.unciv.models.stats.Stat
import com.unciv.ui.components.extensions.randomWeighted
import com.unciv.ui.screens.victoryscreen.RankingType
import kotlin.math.min
import kotlin.math.pow
@ -60,11 +63,63 @@ class CityStateFunctions(val civInfo: Civilization) {
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
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()
/** 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.totalCultureForContests += nextTurnStats.culture.toInt()
if (civInfo.isCityState())
if (civInfo.isCityState()) {
civInfo.questManager.endTurn()
civInfo.cityStateFunctions.nextTurnElections()
}
// disband units until there are none left OR the gold values are normal
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),
Surveillance("Observing City", false, 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)
},
CounterIntelligence("Conducting Counter-intelligence", false, true) {
@ -100,7 +100,7 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
SpyAction.EstablishNetwork -> {
val city = getCity() // This should never throw an exception, as going to the hideout sets your action to None.
if (city.civ.isCityState())
setAction(SpyAction.RiggingElections, 10)
setAction(SpyAction.RiggingElections, getCity().civ.cityStateTurnsUntilElection - 1)
else if (city.civ == civInfo)
setAction(SpyAction.CounterIntelligence, 10)
else
@ -131,7 +131,9 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
}
}
SpyAction.RiggingElections -> {
rigElection()
// No action done here
// Handled in CityStateFunctions.nextTurnElections()
turnsRemainingForAction = getCity().civ.cityStateTurnsUntilElection - 1
}
SpyAction.Dead -> {
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?) {
if (city == null) { // Moving to spy hideout
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.
*/
// 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