mirror of
https://github.com/yairm210/Unciv.git
synced 2025-01-07 05:51:12 +07:00
Extra Civ and Spy moddability (#11702)
* Made minimum duration of a war moddable * Made turns until revolt moddable * Made spy skill moddable * Migrated city-state elections to use the Civ flag system * Moved cityStateElectionTurns away from espionage * Added new moddable constants to the documentation * Fixed merge conflicts
This commit is contained in:
parent
7012297af6
commit
b496784ab5
@ -304,7 +304,6 @@ class Civilization : IsPartOfGameInfoSerialization {
|
||||
toReturn.hasMovedAutomatedUnits = hasMovedAutomatedUnits
|
||||
toReturn.statsHistory = statsHistory.clone()
|
||||
toReturn.resourceStockpiles = resourceStockpiles.clone()
|
||||
toReturn.cityStateTurnsUntilElection = cityStateTurnsUntilElection
|
||||
return toReturn
|
||||
}
|
||||
|
||||
@ -365,7 +364,6 @@ 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) }
|
||||
@ -1014,6 +1012,7 @@ class CivilizationInfoPreview() {
|
||||
|
||||
enum class CivFlags {
|
||||
CityStateGreatPersonGift,
|
||||
TurnsTillCityStateElection,
|
||||
TurnsTillNextDiplomaticVote,
|
||||
ShowDiplomaticVotingResults,
|
||||
ShouldResetDiplomaticVotes,
|
||||
|
@ -64,58 +64,57 @@ class CityStateFunctions(val civInfo: Civilization) {
|
||||
}
|
||||
|
||||
// Set turns to elections to a random number so not every city-state has the same election date
|
||||
civInfo.cityStateTurnsUntilElection = Random.nextInt(15)
|
||||
if (civInfo.gameInfo.isEspionageEnabled()) {
|
||||
civInfo.addFlag(CivFlags.TurnsTillCityStateElection.name, Random.nextInt(civInfo.gameInfo.ruleset.modOptions.constants.cityStateElectionTurns + 1))
|
||||
}
|
||||
|
||||
// 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 holdElections() {
|
||||
civInfo.addFlag(CivFlags.TurnsTillCityStateElection.name, civInfo.gameInfo.ruleset.modOptions.constants.cityStateElectionTurns)
|
||||
val capital = civInfo.getCapital() ?: 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 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.getSkillModifierPercent() * 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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -157,8 +157,8 @@ object DeclareWar {
|
||||
diplomacyManager.updateHasOpenBorders()
|
||||
|
||||
diplomacyManager.removeModifier(DiplomaticModifiers.YearsOfPeace)
|
||||
diplomacyManager.setFlag(DiplomacyFlags.DeclinedPeace, 3)/// AI won't propose peace for 3 turns
|
||||
diplomacyManager.setFlag(DiplomacyFlags.DeclaredWar, 10) // AI won't agree to trade for 10 turns
|
||||
diplomacyManager.setFlag(DiplomacyFlags.DeclinedPeace, diplomacyManager.civInfo.gameInfo.ruleset.modOptions.constants.minimumWarDuration) // AI won't propose peace for 10 turns
|
||||
diplomacyManager.setFlag(DiplomacyFlags.DeclaredWar, diplomacyManager.civInfo.gameInfo.ruleset.modOptions.constants.minimumWarDuration) // AI won't agree to trade for 10 turns
|
||||
diplomacyManager.removeFlag(DiplomacyFlags.BorderConflict)
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,6 @@ import com.unciv.models.stats.Stats
|
||||
import com.unciv.ui.components.MayaCalendar
|
||||
import com.unciv.ui.screens.worldscreen.status.NextTurnProgress
|
||||
import com.unciv.utils.Log
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.random.Random
|
||||
|
||||
@ -126,6 +125,7 @@ class TurnManager(val civInfo: Civilization) {
|
||||
|
||||
when (flag) {
|
||||
CivFlags.RevoltSpawning.name -> doRevoltSpawn()
|
||||
CivFlags.TurnsTillCityStateElection.name -> civInfo.cityStateFunctions.holdElections()
|
||||
}
|
||||
}
|
||||
handleDiplomaticVictoryFlags()
|
||||
@ -166,7 +166,7 @@ class TurnManager(val civInfo: Civilization) {
|
||||
}
|
||||
|
||||
if (!civInfo.hasFlag(CivFlags.RevoltSpawning.name)) {
|
||||
civInfo.addFlag(CivFlags.RevoltSpawning.name, max(getTurnsBeforeRevolt(),1))
|
||||
civInfo.addFlag(CivFlags.RevoltSpawning.name, getTurnsBeforeRevolt().coerceAtLeast(1))
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -223,7 +223,8 @@ class TurnManager(val civInfo: Civilization) {
|
||||
}
|
||||
|
||||
private fun getTurnsBeforeRevolt() =
|
||||
((4 + Random.Default.nextInt(3)) * max(civInfo.gameInfo.speed.modifier, 1f)).toInt()
|
||||
((civInfo.gameInfo.ruleset.modOptions.constants.baseTurnsUntilRevolt + Random.Default.nextInt(3))
|
||||
* civInfo.gameInfo.speed.modifier.coerceAtLeast(1f)).toInt()
|
||||
|
||||
|
||||
fun endTurn(progressBar: NextTurnProgress? = null) {
|
||||
@ -261,7 +262,11 @@ class TurnManager(val civInfo: Civilization) {
|
||||
|
||||
if (civInfo.isCityState()) {
|
||||
civInfo.questManager.endTurn()
|
||||
civInfo.cityStateFunctions.nextTurnElections()
|
||||
// Todo: Remove this later
|
||||
// The purpouse of this addition is to migrate the old election system to the new flag system
|
||||
if (civInfo.gameInfo.isEspionageEnabled() && !civInfo.hasFlag(CivFlags.TurnsTillCityStateElection.name)) {
|
||||
civInfo.addFlag(CivFlags.TurnsTillCityStateElection.name, Random.nextInt(civInfo.gameInfo.ruleset.modOptions.constants.cityStateElectionTurns + 1))
|
||||
}
|
||||
}
|
||||
|
||||
// disband units until there are none left OR the gold values are normal
|
||||
|
@ -87,7 +87,18 @@ class ModConstants {
|
||||
|
||||
var workboatAutomationSearchMaxTiles = 20
|
||||
|
||||
var maxSpyLevel = 3
|
||||
// Civilization
|
||||
var minimumWarDuration = 10
|
||||
var baseTurnsUntilRevolt = 4
|
||||
var cityStateElectionTurns = 15
|
||||
|
||||
// Espionage
|
||||
var maxSpyRank = 3
|
||||
// How much of a skill bonus each rank gives.
|
||||
// Rank 0 is 100%, rank 1 is 130%, and so on for stealing technology.
|
||||
// Half as much for a coup.
|
||||
var spyRankSkillPercentBonus = 30
|
||||
|
||||
|
||||
fun merge(other: ModConstants) {
|
||||
for (field in this::class.java.declaredFields) {
|
||||
|
@ -4,6 +4,7 @@ import com.badlogic.gdx.math.Vector2
|
||||
import com.unciv.Constants
|
||||
import com.unciv.logic.IsPartOfGameInfoSerialization
|
||||
import com.unciv.logic.city.City
|
||||
import com.unciv.logic.civilization.CivFlags
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.civilization.EspionageAction
|
||||
import com.unciv.logic.civilization.NotificationCategory
|
||||
@ -102,7 +103,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, getCity().civ.cityStateTurnsUntilElection - 1)
|
||||
setAction(SpyAction.RiggingElections, (getCity().civ.flagsCountdown[CivFlags.TurnsTillCityStateElection.name] ?: 1) - 1)
|
||||
else if (city.civ == civInfo)
|
||||
setAction(SpyAction.CounterIntelligence, 10)
|
||||
else
|
||||
@ -130,7 +131,9 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
|
||||
SpyAction.RiggingElections -> {
|
||||
// No action done here
|
||||
// Handled in CityStateFunctions.nextTurnElections()
|
||||
turnsRemainingForAction = getCity().civ.cityStateTurnsUntilElection - 1
|
||||
// TODO: Once we remove support for the old flag system we can remove the null check
|
||||
// Our spies might update before the flag is created in the city-state
|
||||
turnsRemainingForAction = (getCity().civ.flagsCountdown[CivFlags.TurnsTillCityStateElection.name] ?: 0) - 1
|
||||
}
|
||||
SpyAction.Coup -> {
|
||||
initiateCoup()
|
||||
@ -184,6 +187,14 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* With the defult spy leveling:
|
||||
* 100 units change one step in results, there are 4 such steps, and the default random spans 300 units and excludes the best result (undetected success).
|
||||
* Thus the return value translates into (return / 3) percent chance to get the very best result, reducing the chance to get the worst result (kill) by the same amount.
|
||||
* The same modifier from defending counter-intelligence spies goes linearly in the opposite direction.
|
||||
* With the range of this function being hardcoded to 30..90 (and 0 for no defensive spy present), ranks cannot guarantee either best or worst outcome.
|
||||
* 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.
|
||||
*/
|
||||
private fun stealTech() {
|
||||
val city = getCity()
|
||||
val otherCiv = city.civ
|
||||
@ -195,10 +206,10 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
|
||||
// Lower is better
|
||||
var spyResult = Random(randomSeed).nextInt(300)
|
||||
// Add our spies experience
|
||||
spyResult -= getSkillModifier()
|
||||
spyResult -= getSkillModifierPercent()
|
||||
// Subtract the experience of the counter intelligence spies
|
||||
val defendingSpy = city.civ.espionageManager.getSpyAssignedToCity(city)
|
||||
spyResult += defendingSpy?.getSkillModifier() ?: 0
|
||||
spyResult += defendingSpy?.getSkillModifierPercent() ?: 0
|
||||
|
||||
val detectionString = when {
|
||||
spyResult >= 200 -> { // The spy was killed in the attempt (should be able to happen even if there's nothing to steal?)
|
||||
@ -309,7 +320,7 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
|
||||
cityState.getAllyCiv()?.let { civInfo.gameInfo.getCivilization(it) }?.espionageManager?.getSpyAssignedToCity(getCity())
|
||||
else null
|
||||
|
||||
val spyRanks = getSkillModifier() - (defendingSpy?.getSkillModifier() ?: 0)
|
||||
val spyRanks = getSkillModifierPercent() - (defendingSpy?.getSkillModifierPercent() ?: 0)
|
||||
successPercentage += spyRanks / 2f // Each rank counts for 15%
|
||||
|
||||
successPercentage = successPercentage.coerceIn(0f, 85f)
|
||||
@ -354,24 +365,16 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
|
||||
fun getLocationName() = getCityOrNull()?.name ?: Constants.spyHideout
|
||||
|
||||
fun levelUpSpy(amount: Int = 1) {
|
||||
if (rank >= civInfo.gameInfo.ruleset.modOptions.constants.maxSpyLevel) return
|
||||
val ranksToLevelUp = amount.coerceAtMost(civInfo.gameInfo.ruleset.modOptions.constants.maxSpyLevel - rank)
|
||||
if (rank >= civInfo.gameInfo.ruleset.modOptions.constants.maxSpyRank) return
|
||||
val ranksToLevelUp = amount.coerceAtMost(civInfo.gameInfo.ruleset.modOptions.constants.maxSpyRank - rank)
|
||||
|
||||
if (ranksToLevelUp == 1) addNotification("Your spy [$name] has leveled up!")
|
||||
else addNotification("Your spy [$name] has leveled up [$ranksToLevelUp] times!")
|
||||
rank += ranksToLevelUp
|
||||
}
|
||||
|
||||
/** Zero-based modifier expressing shift of probabilities from Spy Rank
|
||||
*
|
||||
* 100 units change one step in results, there are 4 such steps, and the default random spans 300 units and excludes the best result (undetected success).
|
||||
* Thus the return value translates into (return / 3) percent chance to get the very best result, reducing the chance to get the worst result (kill) by the same amount.
|
||||
* The same modifier from defending counter-intelligence spies goes linearly in the opposite direction.
|
||||
* With the range of this function being hardcoded to 30..90 (and 0 for no defensive spy present), ranks cannot guarantee either best or worst outcome.
|
||||
* 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?
|
||||
fun getSkillModifier() = rank * 30
|
||||
/** Modifier of the skill bonus of the spy by percent */
|
||||
fun getSkillModifierPercent() = rank * civInfo.gameInfo.ruleset.modOptions.constants.spyRankSkillPercentBonus
|
||||
|
||||
/**
|
||||
* Gets a friendly and enemy efficiency uniques for the spy at the location
|
||||
|
@ -209,6 +209,11 @@ and city distance in another. In case of conflicts, there is no guarantee which
|
||||
| pantheonBase | Int | 10 | [^L] |
|
||||
| pantheonGrowth | Int | 5 | [^L] |
|
||||
| workboatAutomationSearchMaxTiles | Int | 20 | [^M] |
|
||||
| maxSpyRank | Int | 3 | [^N] |
|
||||
| spyRankSkillPercentBonus | Float | 30 | [^O] |
|
||||
| minimumWarDuration | Int | 10 | [^P] |
|
||||
| baseTurnsUntilRevolt | Int | 4 | [^Q] |
|
||||
| cityStateElectionTurns | Int | 15 | [^R] |
|
||||
|
||||
Legend:
|
||||
|
||||
@ -238,7 +243,12 @@ Legend:
|
||||
- [^J]: A [UnitUpgradeCost](#unitupgradecost) sub-structure.
|
||||
- [^K]: Maximum foundable Religions = religionLimitBase + floor(MajorCivCount * religionLimitMultiplier)
|
||||
- [^L]: Cost of pantheon = pantheonBase + CivsWithReligion * pantheonGrowth
|
||||
- [^M]: When the AI decidees whether to build a work boat, how many tiles to search from the city center for an improvable tile
|
||||
- [^M]: When the AI decides whether to build a work boat, how many tiles to search from the city center for an improvable tile
|
||||
- [^N]: The maximum rank any spy can reach
|
||||
- [^O]: How much skill bonus each rank gives
|
||||
- [^P]: The number of turns a civ has to wait before negotiating for peace
|
||||
- [^Q]: The number of turns before a revolt is spawned
|
||||
- [^R]: The number of turns between city-state elections
|
||||
|
||||
#### UnitUpgradeCost
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user