mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-16 18:59:15 +07:00
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:
@ -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. =
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) }
|
||||||
|
@ -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 */
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user