mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-26 05:14:32 -04:00
Spies now occasionally steal technologies (#9629)
* Spies now occasionally steal technologies * Updated the UI so it doesn't show a turn counter when that cannot be provided * Implemented changes discussed in reveiw comments * Renamed variable --------- Co-authored-by: Yair Morgenstern <yairm210@hotmail.com>
This commit is contained in:
parent
3c1f0f7814
commit
47e93a86bf
@ -927,8 +927,6 @@ Your [ourUnit] captured an enemy [theirUnit]! =
|
||||
Your [ourUnit] plundered [amount] [Stat] from [theirUnit] =
|
||||
We have captured a barbarian encampment and recovered [goldAmount] gold! =
|
||||
An enemy [unitType] has joined us! =
|
||||
After an unknown civilization entered the [eraName], we have recruited [spyName] as a spy! =
|
||||
We have recruited [spyName] as a spy! =
|
||||
[unitName] can be promoted! =
|
||||
|
||||
# This might be needed for a rewrite of Germany's unique - see #7376
|
||||
@ -1650,14 +1648,21 @@ Enhancing religion =
|
||||
Enhanced religion =
|
||||
|
||||
# Espionage
|
||||
# As espionage is WIP and these strings are currently not shown in-game,
|
||||
# feel free to not translate these strings for now
|
||||
# As espionage is WIP, these strings are currently not shown in-game,
|
||||
# so feel free to not translate these strings for now
|
||||
|
||||
Spy =
|
||||
Spy Hideout =
|
||||
Spy present =
|
||||
Move =
|
||||
|
||||
After an unknown civilization entered the [eraName], we have recruited [spyName] as a spy! =
|
||||
We have recruited [spyName] as a spy! =
|
||||
A spy from [civilization] stole the Technology [techName] from [cityName]! =
|
||||
An unidentified spy stole the Technology [techName] from [cityName]! =
|
||||
Your spy [name] stole the Technology [techName] from [cityName]! =
|
||||
Your spy [name] cannot steal any more techs from [civilization] as we've already researched all the technology they know! =
|
||||
|
||||
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. =
|
||||
Due to the chaos ensuing in [cityName], your spy [spyname] has fled back to our hideout. =
|
||||
|
@ -13,7 +13,7 @@ enum class SpyFleeReason {
|
||||
Other
|
||||
}
|
||||
|
||||
class CityEspionageManager : IsPartOfGameInfoSerialization{
|
||||
class CityEspionageManager : IsPartOfGameInfoSerialization {
|
||||
@Transient
|
||||
lateinit var city: City
|
||||
|
||||
|
@ -759,13 +759,13 @@ class Civilization : IsPartOfGameInfoSerialization {
|
||||
}
|
||||
}
|
||||
|
||||
fun addNotification(text: String, location: Vector2, category:NotificationCategory, vararg notificationIcons: String) {
|
||||
fun addNotification(text: String, location: Vector2, category: NotificationCategory, vararg notificationIcons: String) {
|
||||
addNotification(text, LocationAction(location), category, *notificationIcons)
|
||||
}
|
||||
|
||||
fun addNotification(text: String, category:NotificationCategory, vararg notificationIcons: String) = addNotification(text, null, category, *notificationIcons)
|
||||
fun addNotification(text: String, category: NotificationCategory, vararg notificationIcons: String) = addNotification(text, null, category, *notificationIcons)
|
||||
|
||||
fun addNotification(text: String, action: NotificationAction?, category:NotificationCategory, vararg notificationIcons: String) {
|
||||
fun addNotification(text: String, action: NotificationAction?, category: NotificationCategory, vararg notificationIcons: String) {
|
||||
if (playerType == PlayerType.AI) return // no point in lengthening the saved game info if no one will read it
|
||||
val arrayList = notificationIcons.toCollection(ArrayList())
|
||||
notifications.add(Notification(text, arrayList,
|
||||
|
@ -40,7 +40,7 @@ object NotificationIcon {
|
||||
const val War = "OtherIcons/Pillage"
|
||||
}
|
||||
|
||||
enum class NotificationCategory{
|
||||
enum class NotificationCategory {
|
||||
General,
|
||||
Trade,
|
||||
Diplomacy,
|
||||
@ -69,7 +69,7 @@ open class Notification() : IsPartOfGameInfoSerialization {
|
||||
var action: NotificationAction? = null
|
||||
var category: String = NotificationCategory.General.name
|
||||
|
||||
constructor(text: String, notificationIcons: ArrayList<String>, action: NotificationAction?, category:NotificationCategory) : this() {
|
||||
constructor(text: String, notificationIcons: ArrayList<String>, action: NotificationAction?, category: NotificationCategory) : this() {
|
||||
this.text = text
|
||||
this.icons = notificationIcons
|
||||
this.action = action
|
||||
|
@ -1,106 +1,21 @@
|
||||
package com.unciv.logic.civilization.managers
|
||||
|
||||
import com.unciv.Constants
|
||||
import com.unciv.logic.IsPartOfGameInfoSerialization
|
||||
import com.unciv.logic.city.City
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
import com.unciv.models.Spy
|
||||
|
||||
enum class SpyAction(val stringName: String) {
|
||||
None("None"),
|
||||
Moving("Moving"),
|
||||
EstablishNetwork("Establishing Network"),
|
||||
StealingTech("Stealing Tech"),
|
||||
RiggingElections("Rigging Elections"),
|
||||
CounterIntelligence("Conducting Counter-intelligence")
|
||||
}
|
||||
|
||||
|
||||
class Spy() : IsPartOfGameInfoSerialization {
|
||||
// `location == null` means that the spy is in its hideout
|
||||
var location: String? = null
|
||||
lateinit var name: String
|
||||
var timeTillActionFinish = 0
|
||||
var action = SpyAction.None
|
||||
|
||||
@Transient
|
||||
lateinit var civInfo: Civilization
|
||||
|
||||
constructor(name: String) : this() {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
fun clone(): Spy {
|
||||
val toReturn = Spy(name)
|
||||
toReturn.location = location
|
||||
toReturn.timeTillActionFinish = timeTillActionFinish
|
||||
toReturn.action = action
|
||||
return toReturn
|
||||
}
|
||||
|
||||
fun setTransients(civInfo: Civilization) {
|
||||
this.civInfo = civInfo
|
||||
}
|
||||
|
||||
fun endTurn() {
|
||||
--timeTillActionFinish
|
||||
if (timeTillActionFinish != 0) return
|
||||
|
||||
when (action) {
|
||||
SpyAction.Moving -> {
|
||||
action = SpyAction.EstablishNetwork
|
||||
timeTillActionFinish = 3 // Dependent on cultural familiarity level if that is ever implemented
|
||||
}
|
||||
SpyAction.EstablishNetwork -> {
|
||||
val location = getLocation()!! // This should be impossible to reach as going to the hideout sets your action to None.
|
||||
action =
|
||||
if (location.civ.isCityState()) {
|
||||
SpyAction.RiggingElections
|
||||
} else if (location.civ == civInfo) {
|
||||
SpyAction.CounterIntelligence
|
||||
} else {
|
||||
SpyAction.StealingTech
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
++timeTillActionFinish // Not implemented yet, so don't do anything
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun moveTo(city: City?) {
|
||||
location = city?.id
|
||||
if (city == null) { // Moving to spy hideout
|
||||
action = SpyAction.None
|
||||
timeTillActionFinish = 0
|
||||
return
|
||||
}
|
||||
action = SpyAction.Moving
|
||||
timeTillActionFinish = 1
|
||||
}
|
||||
|
||||
fun isSetUp() = action !in listOf(SpyAction.Moving, SpyAction.None, SpyAction.EstablishNetwork)
|
||||
|
||||
fun getLocation(): City? {
|
||||
return civInfo.gameInfo.getCities().firstOrNull { it.id == location }
|
||||
}
|
||||
|
||||
fun getLocationName(): String {
|
||||
return getLocation()?.name ?: Constants.spyHideout
|
||||
}
|
||||
}
|
||||
|
||||
class EspionageManager : IsPartOfGameInfoSerialization {
|
||||
|
||||
var spyCount = 0
|
||||
var spyList = mutableListOf<Spy>()
|
||||
var erasSpyEarnedFor = mutableListOf<String>()
|
||||
val erasSpyEarnedFor = mutableSetOf<String>()
|
||||
|
||||
@Transient
|
||||
lateinit var civInfo: Civilization
|
||||
|
||||
fun clone(): EspionageManager {
|
||||
val toReturn = EspionageManager()
|
||||
toReturn.spyCount = spyCount
|
||||
toReturn.spyList.addAll(spyList.map { it.clone() })
|
||||
toReturn.erasSpyEarnedFor.addAll(erasSpyEarnedFor)
|
||||
return toReturn
|
||||
@ -130,7 +45,23 @@ class EspionageManager : IsPartOfGameInfoSerialization {
|
||||
val newSpy = Spy(spyName)
|
||||
newSpy.setTransients(civInfo)
|
||||
spyList.add(newSpy)
|
||||
++spyCount
|
||||
return spyName
|
||||
}
|
||||
|
||||
fun getTilesVisibleViaSpies(): Sequence<Tile> {
|
||||
return spyList.asSequence()
|
||||
.filter { it.isSetUp() }
|
||||
.mapNotNull { it.getLocation() }
|
||||
.flatMap { it.getCenterTile().getTilesInDistance(1) }
|
||||
}
|
||||
|
||||
fun getTechsToSteal(otherCiv: Civilization): Set<String> {
|
||||
val techsToSteal = mutableSetOf<String>()
|
||||
for (tech in otherCiv.tech.techsResearched) {
|
||||
if (civInfo.tech.isResearched(tech)) continue
|
||||
if (!civInfo.tech.canBeResearched(tech)) continue
|
||||
techsToSteal.add(tech)
|
||||
}
|
||||
return techsToSteal
|
||||
}
|
||||
}
|
||||
|
@ -182,11 +182,7 @@ class CivInfoTransientCache(val civInfo: Civilization) {
|
||||
}
|
||||
}
|
||||
|
||||
for (spy in civInfo.espionageManager.spyList) {
|
||||
val spyCity = spy.getLocation() ?: continue
|
||||
if (!spy.isSetUp()) continue // Can't see cities when you haven't set up yet
|
||||
newViewableTiles.addAll(spyCity.getCenterTile().getTilesInDistance(1))
|
||||
}
|
||||
newViewableTiles.addAll(civInfo.espionageManager.getTilesVisibleViaSpies())
|
||||
|
||||
civInfo.viewableTiles = newViewableTiles // to avoid concurrent modification problems
|
||||
}
|
||||
|
164
core/src/com/unciv/models/Spy.kt
Normal file
164
core/src/com/unciv/models/Spy.kt
Normal file
@ -0,0 +1,164 @@
|
||||
package com.unciv.models
|
||||
|
||||
import com.unciv.Constants
|
||||
import com.unciv.logic.IsPartOfGameInfoSerialization
|
||||
import com.unciv.logic.city.City
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.civilization.NotificationCategory
|
||||
import com.unciv.logic.civilization.NotificationIcon
|
||||
import com.unciv.logic.civilization.managers.EspionageManager
|
||||
import kotlin.random.Random
|
||||
|
||||
|
||||
enum class SpyAction(val displayString: String) {
|
||||
None("None"),
|
||||
Moving("Moving"),
|
||||
EstablishNetwork("Establishing Network"),
|
||||
Surveillance("Observing City"),
|
||||
StealingTech("Stealing Tech"),
|
||||
RiggingElections("Rigging Elections"),
|
||||
CounterIntelligence("Conducting Counter-intelligence")
|
||||
}
|
||||
|
||||
|
||||
class Spy() : IsPartOfGameInfoSerialization {
|
||||
// `location == null` means that the spy is in its hideout
|
||||
var location: String? = null
|
||||
lateinit var name: String
|
||||
var action = SpyAction.None
|
||||
private set
|
||||
var turnsRemainingForAction = 0
|
||||
private set
|
||||
private var progressTowardsStealingTech = 0
|
||||
|
||||
@Transient
|
||||
lateinit var civInfo: Civilization
|
||||
|
||||
@Transient
|
||||
private lateinit var espionageManager: EspionageManager
|
||||
|
||||
constructor(name: String) : this() {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
fun clone(): Spy {
|
||||
val toReturn = Spy(name)
|
||||
toReturn.location = location
|
||||
toReturn.action = action
|
||||
toReturn.turnsRemainingForAction = turnsRemainingForAction
|
||||
toReturn.progressTowardsStealingTech = progressTowardsStealingTech
|
||||
return toReturn
|
||||
}
|
||||
|
||||
fun setTransients(civInfo: Civilization) {
|
||||
this.civInfo = civInfo
|
||||
this.espionageManager = civInfo.espionageManager
|
||||
}
|
||||
|
||||
fun endTurn() {
|
||||
when (action) {
|
||||
SpyAction.None -> return
|
||||
SpyAction.Moving -> {
|
||||
--turnsRemainingForAction
|
||||
if (turnsRemainingForAction > 0) return
|
||||
|
||||
action = SpyAction.EstablishNetwork
|
||||
turnsRemainingForAction = 3 // Depending on cultural familiarity level if that is ever implemented
|
||||
}
|
||||
SpyAction.EstablishNetwork -> {
|
||||
--turnsRemainingForAction
|
||||
if (turnsRemainingForAction > 0) return
|
||||
|
||||
val location = getLocation()!! // This should never throw an exception, as going to the hideout sets your action to None.
|
||||
if (location.civ.isCityState()) {
|
||||
action = SpyAction.RiggingElections
|
||||
} else if (location.civ == civInfo) {
|
||||
action = SpyAction.CounterIntelligence
|
||||
} else {
|
||||
startStealingTech()
|
||||
}
|
||||
}
|
||||
SpyAction.Surveillance -> {
|
||||
if (!getLocation()!!.civ.isMajorCiv()) return
|
||||
|
||||
val stealableTechs = espionageManager.getTechsToSteal(getLocation()!!.civ)
|
||||
if (stealableTechs.isEmpty()) return
|
||||
action = SpyAction.StealingTech // There are new techs to steal!
|
||||
}
|
||||
SpyAction.StealingTech -> {
|
||||
val stealableTechs = espionageManager.getTechsToSteal(getLocation()!!.civ)
|
||||
if (stealableTechs.isEmpty()) {
|
||||
action = SpyAction.Surveillance
|
||||
turnsRemainingForAction = 0
|
||||
val notificationString = "Your spy [$name] cannot steal any more techs from [${getLocation()!!.civ}] as we've already researched all the technology they know!"
|
||||
civInfo.addNotification(notificationString, getLocation()!!.location, NotificationCategory.Espionage, NotificationIcon.Spy)
|
||||
return
|
||||
}
|
||||
val techStealCost = stealableTechs.maxOfOrNull { civInfo.gameInfo.ruleset.technologies[it]!!.cost }!!
|
||||
val progressThisTurn = getLocation()!!.cityStats.currentCityStats.science
|
||||
progressTowardsStealingTech += progressThisTurn.toInt()
|
||||
if (progressTowardsStealingTech > techStealCost) {
|
||||
stealTech()
|
||||
}
|
||||
}
|
||||
else -> return // Not implemented yet, so don't do anything
|
||||
}
|
||||
}
|
||||
|
||||
fun startStealingTech() {
|
||||
action = SpyAction.StealingTech
|
||||
turnsRemainingForAction = 0
|
||||
progressTowardsStealingTech = 0
|
||||
}
|
||||
|
||||
private fun stealTech() {
|
||||
val city = getLocation()!!
|
||||
val otherCiv = city.civ
|
||||
val randomSeed = city.location.x * city.location.y + 123f * civInfo.gameInfo.turns
|
||||
|
||||
val stolenTech = espionageManager.getTechsToSteal(getLocation()!!.civ)
|
||||
.randomOrNull(Random(randomSeed.toInt())) // Could be improved to for example steal the most expensive tech or the tech that has the least progress as of yet
|
||||
if (stolenTech != null) {
|
||||
civInfo.tech.addTechnology(stolenTech)
|
||||
}
|
||||
|
||||
val spyDetected = Random(randomSeed.toInt()).nextInt(3)
|
||||
val detectionString = when (spyDetected) {
|
||||
0 -> "A spy from [${civInfo.civName}] stole the Technology [$stolenTech] from [$city]!"
|
||||
1 -> "An unidentified spy stole the Technology [$stolenTech] from [$city]!"
|
||||
else -> null // Not detected
|
||||
}
|
||||
if (detectionString != null)
|
||||
otherCiv.addNotification(detectionString, city.location, NotificationCategory.Espionage, NotificationIcon.Spy)
|
||||
|
||||
civInfo.addNotification("Your spy [$name] stole the Technology [$stolenTech] from [$city]!", city.location,
|
||||
NotificationCategory.Espionage,
|
||||
NotificationIcon.Spy
|
||||
)
|
||||
|
||||
|
||||
startStealingTech()
|
||||
}
|
||||
|
||||
|
||||
fun moveTo(city: City?) {
|
||||
location = city?.id
|
||||
if (city == null) { // Moving to spy hideout
|
||||
action = SpyAction.None
|
||||
turnsRemainingForAction = 0
|
||||
return
|
||||
}
|
||||
action = SpyAction.Moving
|
||||
turnsRemainingForAction = 1
|
||||
}
|
||||
|
||||
fun isSetUp() = action !in listOf(SpyAction.Moving, SpyAction.None, SpyAction.EstablishNetwork)
|
||||
|
||||
fun getLocation(): City? {
|
||||
return civInfo.gameInfo.getCities().firstOrNull { it.id == location }
|
||||
}
|
||||
|
||||
fun getLocationName(): String {
|
||||
return getLocation()?.name ?: Constants.spyHideout
|
||||
}
|
||||
}
|
@ -622,9 +622,9 @@ object UniqueTriggerActivation {
|
||||
otherCiv.espionageManager.erasSpyEarnedFor.add(currentEra)
|
||||
if (otherCiv == civInfo || otherCiv.knows(civInfo))
|
||||
// We don't tell which civilization entered the new era, as that is done in the notification directly above this one
|
||||
otherCiv.addNotification("We have recruited [${spyName}] as a spy!", NotificationCategory.General, NotificationIcon.Spy)
|
||||
otherCiv.addNotification("We have recruited [${spyName}] as a spy!", NotificationCategory.Espionage, NotificationIcon.Spy)
|
||||
else
|
||||
otherCiv.addNotification("After an unknown civilization entered the [${currentEra}], we have recruited [${spyName}] as a spy!", NotificationCategory.General, NotificationIcon.Spy)
|
||||
otherCiv.addNotification("After an unknown civilization entered the [${currentEra}], we have recruited [${spyName}] as a spy!", NotificationCategory.Espionage, NotificationIcon.Spy)
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
@ -225,8 +225,7 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
|
||||
UnitSupplyPerCity("[amount] Unit Supply per city", UniqueTarget.Global),
|
||||
FreeUnits("[amount] units cost no maintenance", UniqueTarget.Global),
|
||||
UnitsInCitiesNoMaintenance("Units in cities cost no Maintenance", UniqueTarget.Global),
|
||||
// Acts as a trigger - this should be generalized somehow but the current setup does not allow this
|
||||
// It would currently mean cycling through EVERY unique type to find ones with a specific conditional...
|
||||
// ToDo: Replace with "Free [unit] appears <upon discovering [tech]>"
|
||||
ReceiveFreeUnitWhenDiscoveringTech("Receive free [unit] when you discover [tech]", UniqueTarget.Global),
|
||||
|
||||
// Units entering Tiles
|
||||
|
@ -5,7 +5,7 @@ import com.badlogic.gdx.files.FileHandle
|
||||
import com.unciv.json.fromJsonFile
|
||||
import com.unciv.json.json
|
||||
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
|
||||
import com.unciv.logic.civilization.managers.SpyAction
|
||||
import com.unciv.models.SpyAction
|
||||
import com.unciv.models.metadata.BaseRuleset
|
||||
import com.unciv.models.metadata.LocaleCode
|
||||
import com.unciv.models.ruleset.Belief
|
||||
@ -127,7 +127,7 @@ object TranslationFileWriter {
|
||||
|
||||
linesToTranslate += "\n\n#################### Lines from spy actions #######################\n"
|
||||
for (spyAction in SpyAction.values()) {
|
||||
linesToTranslate += "$spyAction = "
|
||||
linesToTranslate += "${spyAction.displayString} = "
|
||||
}
|
||||
|
||||
linesToTranslate += "\n\n#################### Lines from diplomatic modifiers #######################\n"
|
||||
|
@ -8,8 +8,8 @@ import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.city.City
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.civilization.managers.Spy
|
||||
import com.unciv.logic.civilization.managers.SpyAction
|
||||
import com.unciv.models.Spy
|
||||
import com.unciv.models.SpyAction
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.screens.pickerscreens.PickerScreen
|
||||
@ -71,8 +71,12 @@ class EspionageOverviewScreen(val civInfo: Civilization) : PickerScreen(true) {
|
||||
spySelectionTable.add(spy.name.toLabel()).pad(10f)
|
||||
spySelectionTable.add(spy.getLocationName().toLabel()).pad(10f)
|
||||
val actionString =
|
||||
if (spy.action == SpyAction.None) SpyAction.None.stringName
|
||||
else "[${spy.action.stringName}] ${spy.timeTillActionFinish}${Fonts.turn}"
|
||||
when (spy.action) {
|
||||
SpyAction.None, SpyAction.StealingTech, SpyAction.Surveillance -> spy.action.displayString
|
||||
SpyAction.Moving, SpyAction.EstablishNetwork -> "[${spy.action.displayString}] ${spy.turnsRemainingForAction}${Fonts.turn}"
|
||||
SpyAction.RiggingElections -> TODO()
|
||||
SpyAction.CounterIntelligence -> TODO()
|
||||
}
|
||||
spySelectionTable.add(actionString.toLabel()).pad(10f)
|
||||
|
||||
val moveSpyButton = "Move".toTextButton()
|
||||
|
@ -153,7 +153,7 @@ class TechPolicyDiplomacyButtons(val worldScreen: WorldScreen) : Table(BaseScree
|
||||
}
|
||||
|
||||
private fun updateEspionageButton() {
|
||||
if (viewingCiv.espionageManager.spyCount == 0) {
|
||||
if (viewingCiv.espionageManager.spyList.isEmpty()) {
|
||||
espionageButtonHolder.touchable = Touchable.disabled
|
||||
espionageButtonHolder.actor = null
|
||||
} else {
|
||||
|
Loading…
x
Reference in New Issue
Block a user