Added a screen to move spies between cities (#7665)

* Added a screen to move spies between cities

* Fixed tests

* Reviews

* Avoid labels blinking

Co-authored-by: JackRainy <JackRainy@users.noreply.github.com>
This commit is contained in:
Xander Lenstra
2022-08-28 22:25:14 +02:00
committed by GitHub
parent 3202239129
commit a19fed5d28
17 changed files with 845 additions and 594 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -1453,6 +1453,15 @@ Religion =
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
Spy =
Spy Hideout =
Location =
Spy present =
Move =
# Promotions

View File

@ -56,6 +56,7 @@ object Constants {
// Easter egg name. Is to avoid conflicts when players name their own religions.
// This religion name should never be displayed.
const val noReligionName = "The religion of TheLegend27"
const val spyHideout = "Spy Hideout"
const val neutralVictoryType = "Neutral"

View File

@ -0,0 +1,22 @@
package com.unciv.logic.city
import com.unciv.logic.IsPartOfGameInfoSerialization
import com.unciv.logic.civilization.CivilizationInfo
class CityEspionageManager : IsPartOfGameInfoSerialization{
@Transient
lateinit var cityInfo: CityInfo
fun clone(): CityEspionageManager {
return CityEspionageManager()
}
fun setTransients(cityInfo: CityInfo) {
this.cityInfo = cityInfo
}
fun hasSpyOf(civInfo: CivilizationInfo): Boolean {
return civInfo.espionageManager.spyList.any { it.location == cityInfo.id }
}
}

View File

@ -110,10 +110,11 @@ class CityInfo : IsPartOfGameInfoSerialization {
var health = 200
var religion = CityInfoReligionManager()
var population = PopulationManager()
var cityConstructions = CityConstructions()
var expansion = CityExpansionManager()
var religion = CityReligionManager()
var espionage = CityEspionageManager()
@Transient // CityStats has no serializable fields
var cityStats = CityStats(this)
@ -628,6 +629,7 @@ class CityInfo : IsPartOfGameInfoSerialization {
cityConstructions.cityInfo = this
cityConstructions.setTransients()
religion.setTransients(this)
espionage.setTransients(this)
}
fun startTurn() {

View File

@ -9,7 +9,7 @@ import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.utils.extensions.toPercent
class CityInfoReligionManager : IsPartOfGameInfoSerialization {
class CityReligionManager : IsPartOfGameInfoSerialization {
@Transient
lateinit var cityInfo: CityInfo
@ -32,8 +32,8 @@ class CityInfoReligionManager : IsPartOfGameInfoSerialization {
clearAllPressures()
}
fun clone(): CityInfoReligionManager {
val toReturn = CityInfoReligionManager()
fun clone(): CityReligionManager {
val toReturn = CityReligionManager()
toReturn.cityInfo = cityInfo
toReturn.religionsAtSomePointAdopted.addAll(religionsAtSomePointAdopted)
toReturn.pressures.putAll(pressures)

View File

@ -1,8 +1,13 @@
package com.unciv.logic.civilization
import com.unciv.Constants
import com.unciv.logic.GameInfo
import com.unciv.logic.IsPartOfGameInfoSerialization
import com.unciv.logic.city.CityInfo
class Spy() : IsPartOfGameInfoSerialization {
// `location == null` means that the spy is in its hideout
var location: String? = null
lateinit var name: String
constructor(name: String) : this() {
@ -10,7 +15,17 @@ class Spy() : IsPartOfGameInfoSerialization {
}
fun clone(): Spy {
return Spy(name)
val toReturn = Spy(name)
toReturn.location = location
return toReturn
}
fun getLocation(gameInfo: GameInfo): CityInfo? {
return gameInfo.getCities().firstOrNull { it.id == location }
}
fun getLocationName(gameInfo: GameInfo): String {
return getLocation(gameInfo)?.name ?: Constants.spyHideout
}
}
@ -45,6 +60,7 @@ class EspionageManager : IsPartOfGameInfoSerialization {
fun addSpy(): String {
val spyName = getSpyName()
spyList.add(Spy(spyName))
++spyCount
return spyName
}
}

View File

@ -331,6 +331,7 @@ class TechManager : IsPartOfGameInfoSerialization {
for (policyBranch in getRuleset().policyBranches.values.filter { it.era == currentEra.name && civInfo.policies.isAdoptable(it) }) {
civInfo.addNotification("[" + policyBranch.name + "] policy branch unlocked!", NotificationIcon.Culture)
}
// Note that if you somehow skip over an era, its uniques aren't triggered
for (unique in currentEra.uniqueObjects) {
UniqueTriggerActivation.triggerCivwideUnique(unique, civInfo)
}

View File

@ -216,7 +216,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
return 0
}
@Suppress("unused") //todo Finish original intent or remove
@Suppress("unused") //todo Finish original intent (usage in uniques) or remove
fun matchesCityStateRelationshipFilter(filter: String): Boolean {
val relationshipLevel = relationshipLevel()
return when (filter) {

View File

@ -487,7 +487,7 @@ fun String.removeConditionals(): String {
return this
.replace(pointyBraceRegex, "")
// So, this is a quick hack, but it works as long as nobody uses word separators different from " " (space) and "" (none),
// And no translations start or end with a space.
// and no translations start or end with a space.
// According to https://linguistics.stackexchange.com/questions/6131/is-there-a-long-list-of-languages-whose-writing-systems-dont-use-spaces
// This is a reasonable but not fully correct assumption to make.
// By doing it like this, we exclude languages such as Tibetan, Dzongkha (Bhutan), and Ethiopian.

View File

@ -4,7 +4,7 @@ import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.city.CityInfoReligionManager
import com.unciv.logic.city.CityReligionManager
import com.unciv.models.Religion
import com.unciv.ui.civilopedia.CivilopediaCategories
import com.unciv.ui.civilopedia.CivilopediaScreen
@ -20,7 +20,7 @@ import com.unciv.ui.utils.extensions.onClick
import com.unciv.ui.utils.extensions.toLabel
class CityReligionInfoTable(
private val religionManager: CityInfoReligionManager,
private val religionManager: CityReligionManager,
showMajority: Boolean = false
) : Table(BaseScreen.skin) {
private val civInfo = religionManager.cityInfo.civInfo

View File

@ -0,0 +1,170 @@
package com.unciv.ui.overviewscreen
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Button
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.utils.Align
import com.unciv.UncivGame
import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.civilization.Spy
import com.unciv.models.translations.tr
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.pickerscreens.PickerScreen
import com.unciv.ui.utils.AutoScrollPane
import com.unciv.ui.utils.extensions.addSeparator
import com.unciv.ui.utils.extensions.addSeparatorVertical
import com.unciv.ui.utils.extensions.onClick
import com.unciv.ui.utils.extensions.setSize
import com.unciv.ui.utils.extensions.toLabel
import com.unciv.ui.utils.extensions.toTextButton
/** Screen used for moving spies between cities */
class EspionageOverviewScreen(val civInfo: CivilizationInfo) : PickerScreen(true) {
private val collator = UncivGame.Current.settings.getCollatorFromLocale()
private val spySelectionTable = Table(skin)
private val spyScrollPane = AutoScrollPane(spySelectionTable)
private val citySelectionTable = Table(skin)
private val cityScrollPane = AutoScrollPane(citySelectionTable)
private val headerTable = Table(skin)
private val middlePanes = Table(skin)
private var selectedSpyButton: TextButton? = null
private var selectedSpy: Spy? = null
// if the value == null, this means the Spy Hideout.
private var moveSpyHereButtons = hashMapOf<Button,CityInfo?>()
init {
topTable.add(headerTable)
topTable.addSeparator()
middlePanes.add(spyScrollPane)
middlePanes.addSeparatorVertical()
middlePanes.add(cityScrollPane)
topTable.add(middlePanes)
update()
closeButton.isVisible = true
setDefaultCloseAction()
}
private fun update() {
updateSpyList()
updateCityList()
}
private fun updateSpyList() {
spySelectionTable.clear()
spySelectionTable.add("Spy".toLabel()).pad(5f)
spySelectionTable.add("Location".toLabel()).pad(5f).row()
for (spy in civInfo.espionageManager.spyList) {
spySelectionTable.add(spy.name.toLabel()).pad(5f)
spySelectionTable.add(spy.getLocationName(civInfo.gameInfo).toLabel()).pad(5f)
val moveSpyButton = "Move".toTextButton()
moveSpyButton.onClick {
if (selectedSpyButton == moveSpyButton) {
resetSelection()
return@onClick
}
resetSelection()
selectedSpyButton = moveSpyButton
selectedSpy = spy
selectedSpyButton!!.label.setText("Cancel".tr())
for ((button, city) in moveSpyHereButtons)
// For now, only allow spies to be send to cities of other major civs
// Not own cities as counterintelligence isn't implemented
// Not city-state civs as rigging elections isn't implemented
// Technically, stealing techs from other civs also isn't implemented, but its the first thing I'll add so this makes the most sense to allow.
if (city == null || ( // hideout
city.civInfo.isMajorCiv()
&& city.civInfo != civInfo
&& !city.espionage.hasSpyOf(civInfo)
)) {
button.isVisible = true
}
}
spySelectionTable.add(moveSpyButton).pad(5f).row()
}
}
private fun updateCityList() {
citySelectionTable.clear()
moveSpyHereButtons.clear()
citySelectionTable.add().pad(5f)
citySelectionTable.add("City".toLabel()).pad(5f)
citySelectionTable.add("Spy present".toLabel()).pad(5f).row()
// First add the hideout to the table
citySelectionTable.add().pad(5f)
citySelectionTable.add("Spy Hideout".toLabel()).pad(5f)
citySelectionTable.add().pad(5f)
val moveSpyHereButton = getMoveToCityButton(null)
citySelectionTable.add(moveSpyHereButton).row()
// Then add all cities
val sortedCities = civInfo.gameInfo.getCities()
.filter { it.getCenterTile().position in civInfo.exploredTiles }
.sortedWith(
compareBy<CityInfo> {
it.civInfo != civInfo
}.thenBy {
it.civInfo.isCityState()
}.thenBy(collator) {
it.civInfo.civName.tr()
}.thenBy(collator) {
it.name.tr()
}
)
for (city in sortedCities) {
addCityToSelectionTable(city)
}
}
private fun addCityToSelectionTable(city: CityInfo) {
citySelectionTable.add(ImageGetter.getNationIndicator(city.civInfo.nation, 30f)).pad(5f)
citySelectionTable.add(city.name.toLabel()).pad(5f)
if (city.espionage.hasSpyOf(civInfo)) {
citySelectionTable.add(
ImageGetter.getImage("OtherIcons/Spy_White").apply {
setSize(30f)
color = Color.WHITE
}
).pad(5f)
} else {
citySelectionTable.add().pad(5f)
}
val moveSpyHereButton = getMoveToCityButton(city)
citySelectionTable.add(moveSpyHereButton).pad(5f)
citySelectionTable.row()
}
// city == null is interpreted as 'spy hideout'
private fun getMoveToCityButton(city: CityInfo?) : Button {
val moveSpyHereButton = Button(skin)
moveSpyHereButton.add(ImageGetter.getArrowImage(Align.left).apply { color = Color.WHITE })
moveSpyHereButton.onClick {
selectedSpy!!.location = city?.id
resetSelection()
update()
}
moveSpyHereButtons[moveSpyHereButton] = city
moveSpyHereButton.isVisible = false
return moveSpyHereButton
}
private fun resetSelection() {
selectedSpy = null
if (selectedSpyButton != null)
selectedSpyButton!!.label.setText("Move".tr())
selectedSpyButton = null
for ((button, _) in moveSpyHereButtons)
button.isVisible = false
}
}

View File

@ -15,11 +15,11 @@ import com.badlogic.gdx.scenes.scene2d.utils.ClickListener
** Approach **
Listen to enter and exit events and set focus as needed.
The old focus is saved on eneter and restored on exit to make this as side-effect free as possible.
The old focus is saved on enter and restored on exit to make this as side-effect free as possible.
** Implementation **
The listener is attached per widget (and not, say, to an upper container or the screen, where
one listener would suffice but we'd have to do coordinate to target resolution outselves).
one listener would suffice but we'd have to do coordinate to target resolution ourselves).
This is accomplished by subclassing the ScrollPane and replacing usages,
which in turn can be done either by using this class as drop-in replacement per widget
or by importing this using an import alias per file.
@ -53,4 +53,4 @@ open class AutoScrollPane(widget: Actor?, style: ScrollPaneStyle = ScrollPaneSty
}
})
}
}
}

View File

@ -8,6 +8,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.models.UncivSound
import com.unciv.models.translations.tr
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.overviewscreen.EspionageOverviewScreen
import com.unciv.ui.pickerscreens.PolicyPickerScreen
import com.unciv.ui.pickerscreens.TechButton
import com.unciv.ui.pickerscreens.TechPickerScreen
@ -29,15 +30,18 @@ class TechPolicyDiplomacyButtons(val worldScreen: WorldScreen) : Table(BaseScree
private val policyScreenButton = Button(skin)
private val diplomacyButtonHolder = Container<Button?>()
private val diplomacyButton = Button(skin)
private val espionageButtonHolder = Container<Button?>()
private val espionageButton = Button(skin)
private val viewingCiv = worldScreen.viewingCiv
private val game = worldScreen.game
init {
defaults().left()
add(techButtonHolder).colspan(3).row()
add(techButtonHolder).colspan(4).row()
add(policyButtonHolder).padTop(10f).padRight(10f)
add(diplomacyButtonHolder).padTop(10f)
add(diplomacyButtonHolder).padTop(10f).padRight(10f)
add(espionageButtonHolder).padTop(10f)
add().growX() // Allows Policy and Diplo buttons to keep to the left
pickTechButton.background = ImageGetter.getRoundedEdgeRectangle(colorFromRGB(7, 46, 43))
@ -56,12 +60,20 @@ class TechPolicyDiplomacyButtons(val worldScreen: WorldScreen) : Table(BaseScree
diplomacyButtonHolder.onClick {
game.pushScreen(DiplomacyScreen(viewingCiv))
}
if (game.gameInfo!!.isEspionageEnabled()) {
espionageButton.add(ImageGetter.getImage("OtherIcons/Spy_White")).size(30f).pad(15f)
espionageButtonHolder.onClick {
game.pushScreen(EspionageOverviewScreen(viewingCiv))
}
}
}
fun update(): Boolean {
updateTechButton()
updatePolicyButton()
val result = updateDiplomacyButton()
if (game.gameInfo!!.isEspionageEnabled())
updateEspionageButton()
pack()
setPosition(10f, worldScreen.topBar.y - height - 15f)
return result
@ -114,4 +126,14 @@ class TechPolicyDiplomacyButtons(val worldScreen: WorldScreen) : Table(BaseScree
true
}
}
private fun updateEspionageButton() {
if (viewingCiv.espionageManager.spyCount == 0) {
espionageButtonHolder.touchable = Touchable.disabled
espionageButtonHolder.actor = null
} else {
espionageButtonHolder.touchable = Touchable.enabled
espionageButtonHolder.actor = espionageButton
}
}
}

View File

@ -686,6 +686,7 @@ Unless otherwise specified, all the following are from [the Noun Project](https:
- [favor](https://thenounproject.com/icon/favor-1029350/) by MICHAEL G BROWN for WLTK marker on City Overview
- [Party](https://thenounproject.com/icon/party-1784941/) by Adrien Coquet for WLTK header on City Overview
- [Party](https://thenounproject.com/icon/party-2955155/) by Lars Meiertoberens as additional WLKT decoration
- [spy](https://thenounproject.com/icon/spy-2905374/) by Vectorstall for Spy
- [turn right](https://thenounproject.com/icon/turn-right-1920867/) by Alice Design for Resource Overview
- [Tyrannosaurus Rex](https://thenounproject.com/icon/tyrannosaurus-rex-4130976/) by Amethyst Studio for Civilopedia Eras header
- [Realistic easter day eggs with curvy lines and dots](https://www.freepik.com/free-vector/realistic-easter-day-eggs-with-curvy-lines-dots_6839373.htm) by freepik