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:
Xander Lenstra 2023-06-23 07:51:34 +02:00 committed by GitHub
parent 3c1f0f7814
commit 47e93a86bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 214 additions and 115 deletions

View File

@ -927,8 +927,6 @@ Your [ourUnit] captured an enemy [theirUnit]! =
Your [ourUnit] plundered [amount] [Stat] from [theirUnit] = Your [ourUnit] plundered [amount] [Stat] from [theirUnit] =
We have captured a barbarian encampment and recovered [goldAmount] gold! = We have captured a barbarian encampment and recovered [goldAmount] gold! =
An enemy [unitType] has joined us! = 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! = [unitName] can be promoted! =
# This might be needed for a rewrite of Germany's unique - see #7376 # This might be needed for a rewrite of Germany's unique - see #7376
@ -1650,14 +1648,21 @@ Enhancing religion =
Enhanced religion = Enhanced religion =
# Espionage # Espionage
# As espionage is WIP and these strings are currently not shown in-game, # As espionage is WIP, these strings are currently not shown in-game,
# feel free to not translate these strings for now # so feel free to not translate these strings for now
Spy = Spy =
Spy Hideout = Spy Hideout =
Spy present = Spy present =
Move = 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 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. = 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. = Due to the chaos ensuing in [cityName], your spy [spyname] has fled back to our hideout. =

View File

@ -13,7 +13,7 @@ enum class SpyFleeReason {
Other Other
} }
class CityEspionageManager : IsPartOfGameInfoSerialization{ class CityEspionageManager : IsPartOfGameInfoSerialization {
@Transient @Transient
lateinit var city: City lateinit var city: City

View File

@ -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) 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 if (playerType == PlayerType.AI) return // no point in lengthening the saved game info if no one will read it
val arrayList = notificationIcons.toCollection(ArrayList()) val arrayList = notificationIcons.toCollection(ArrayList())
notifications.add(Notification(text, arrayList, notifications.add(Notification(text, arrayList,

View File

@ -40,7 +40,7 @@ object NotificationIcon {
const val War = "OtherIcons/Pillage" const val War = "OtherIcons/Pillage"
} }
enum class NotificationCategory{ enum class NotificationCategory {
General, General,
Trade, Trade,
Diplomacy, Diplomacy,
@ -69,7 +69,7 @@ open class Notification() : IsPartOfGameInfoSerialization {
var action: NotificationAction? = null var action: NotificationAction? = null
var category: String = NotificationCategory.General.name 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.text = text
this.icons = notificationIcons this.icons = notificationIcons
this.action = action this.action = action

View File

@ -1,106 +1,21 @@
package com.unciv.logic.civilization.managers package com.unciv.logic.civilization.managers
import com.unciv.Constants
import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.IsPartOfGameInfoSerialization
import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization 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 { class EspionageManager : IsPartOfGameInfoSerialization {
var spyCount = 0
var spyList = mutableListOf<Spy>() var spyList = mutableListOf<Spy>()
var erasSpyEarnedFor = mutableListOf<String>() val erasSpyEarnedFor = mutableSetOf<String>()
@Transient @Transient
lateinit var civInfo: Civilization lateinit var civInfo: Civilization
fun clone(): EspionageManager { fun clone(): EspionageManager {
val toReturn = EspionageManager() val toReturn = EspionageManager()
toReturn.spyCount = spyCount
toReturn.spyList.addAll(spyList.map { it.clone() }) toReturn.spyList.addAll(spyList.map { it.clone() })
toReturn.erasSpyEarnedFor.addAll(erasSpyEarnedFor) toReturn.erasSpyEarnedFor.addAll(erasSpyEarnedFor)
return toReturn return toReturn
@ -130,7 +45,23 @@ class EspionageManager : IsPartOfGameInfoSerialization {
val newSpy = Spy(spyName) val newSpy = Spy(spyName)
newSpy.setTransients(civInfo) newSpy.setTransients(civInfo)
spyList.add(newSpy) spyList.add(newSpy)
++spyCount
return spyName 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
}
} }

View File

@ -182,11 +182,7 @@ class CivInfoTransientCache(val civInfo: Civilization) {
} }
} }
for (spy in civInfo.espionageManager.spyList) { newViewableTiles.addAll(civInfo.espionageManager.getTilesVisibleViaSpies())
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))
}
civInfo.viewableTiles = newViewableTiles // to avoid concurrent modification problems civInfo.viewableTiles = newViewableTiles // to avoid concurrent modification problems
} }

View 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
}
}

View File

@ -622,9 +622,9 @@ object UniqueTriggerActivation {
otherCiv.espionageManager.erasSpyEarnedFor.add(currentEra) otherCiv.espionageManager.erasSpyEarnedFor.add(currentEra)
if (otherCiv == civInfo || otherCiv.knows(civInfo)) 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 // 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 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 return true

View File

@ -225,8 +225,7 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
UnitSupplyPerCity("[amount] Unit Supply per city", UniqueTarget.Global), UnitSupplyPerCity("[amount] Unit Supply per city", UniqueTarget.Global),
FreeUnits("[amount] units cost no maintenance", UniqueTarget.Global), FreeUnits("[amount] units cost no maintenance", UniqueTarget.Global),
UnitsInCitiesNoMaintenance("Units in cities 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 // ToDo: Replace with "Free [unit] appears <upon discovering [tech]>"
// It would currently mean cycling through EVERY unique type to find ones with a specific conditional...
ReceiveFreeUnitWhenDiscoveringTech("Receive free [unit] when you discover [tech]", UniqueTarget.Global), ReceiveFreeUnitWhenDiscoveringTech("Receive free [unit] when you discover [tech]", UniqueTarget.Global),
// Units entering Tiles // Units entering Tiles

View File

@ -5,7 +5,7 @@ import com.badlogic.gdx.files.FileHandle
import com.unciv.json.fromJsonFile import com.unciv.json.fromJsonFile
import com.unciv.json.json import com.unciv.json.json
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers 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.BaseRuleset
import com.unciv.models.metadata.LocaleCode import com.unciv.models.metadata.LocaleCode
import com.unciv.models.ruleset.Belief import com.unciv.models.ruleset.Belief
@ -127,7 +127,7 @@ object TranslationFileWriter {
linesToTranslate += "\n\n#################### Lines from spy actions #######################\n" linesToTranslate += "\n\n#################### Lines from spy actions #######################\n"
for (spyAction in SpyAction.values()) { for (spyAction in SpyAction.values()) {
linesToTranslate += "$spyAction = " linesToTranslate += "${spyAction.displayString} = "
} }
linesToTranslate += "\n\n#################### Lines from diplomatic modifiers #######################\n" linesToTranslate += "\n\n#################### Lines from diplomatic modifiers #######################\n"

View File

@ -8,8 +8,8 @@ import com.badlogic.gdx.utils.Align
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.city.City import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.managers.Spy import com.unciv.models.Spy
import com.unciv.logic.civilization.managers.SpyAction import com.unciv.models.SpyAction
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.pickerscreens.PickerScreen 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.name.toLabel()).pad(10f)
spySelectionTable.add(spy.getLocationName().toLabel()).pad(10f) spySelectionTable.add(spy.getLocationName().toLabel()).pad(10f)
val actionString = val actionString =
if (spy.action == SpyAction.None) SpyAction.None.stringName when (spy.action) {
else "[${spy.action.stringName}] ${spy.timeTillActionFinish}${Fonts.turn}" 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) spySelectionTable.add(actionString.toLabel()).pad(10f)
val moveSpyButton = "Move".toTextButton() val moveSpyButton = "Move".toTextButton()

View File

@ -153,7 +153,7 @@ class TechPolicyDiplomacyButtons(val worldScreen: WorldScreen) : Table(BaseScree
} }
private fun updateEspionageButton() { private fun updateEspionageButton() {
if (viewingCiv.espionageManager.spyCount == 0) { if (viewingCiv.espionageManager.spyList.isEmpty()) {
espionageButtonHolder.touchable = Touchable.disabled espionageButtonHolder.touchable = Touchable.disabled
espionageButtonHolder.actor = null espionageButtonHolder.actor = null
} else { } else {