mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-04 23:40:01 +07:00
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:
BIN
android/Images/OtherIcons/Spy_White.png
Normal file
BIN
android/Images/OtherIcons/Spy_White.png
Normal file
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 |
@ -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
|
||||
|
||||
|
@ -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"
|
||||
|
||||
|
22
core/src/com/unciv/logic/city/CityEspionageManager.kt
Normal file
22
core/src/com/unciv/logic/city/CityEspionageManager.kt
Normal 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 }
|
||||
}
|
||||
|
||||
}
|
@ -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() {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
170
core/src/com/unciv/ui/overviewscreen/EspionageOverviewScreen.kt
Normal file
170
core/src/com/unciv/ui/overviewscreen/EspionageOverviewScreen.kt
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user