A few more useful notification actions (#9811)

* Minor UI tweaks - mainly duplicate icons on ResourcesOverviewTab and EspionageOverviewScreen

* Bugfix and expand NotificationActions

* Switch NotificationAction migration to Phase IV

* Tweak a few Notifications to have more useful actions

* Remove one `run {}`

* Better predictability of clicks on Notifications pulled out of history

* Unit creation notifications can now select the unit

* Linting

* ClearBarbarianCamp quest Notification shows map location first

* More Linting

* Hide City-state call for help from aggressor
This commit is contained in:
SomeTroglodyte 2023-08-24 09:09:57 +02:00 committed by GitHub
parent 40fe93888f
commit 12e3cfc5b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 190 additions and 121 deletions

View File

@ -9,10 +9,12 @@ import com.unciv.logic.city.City
import com.unciv.logic.civilization.AlertType
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.LocationAction
import com.unciv.logic.civilization.MapUnitAction
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.PlayerType
import com.unciv.logic.civilization.PopupAlert
import com.unciv.logic.civilization.PromoteUnitAction
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
import com.unciv.logic.map.mapunit.MapUnit
@ -582,8 +584,12 @@ object Battle {
civ.greatPeople.greatGeneralPoints += greatGeneralPointsGained
}
if (!thisCombatant.isDefeated() && !unitCouldAlreadyPromote && promotions.canBePromoted())
civ.addNotification("[${thisCombatant.unit.displayName()}] can be promoted!",thisCombatant.getTile().position, NotificationCategory.Units, thisCombatant.unit.name)
if (!thisCombatant.isDefeated() && !unitCouldAlreadyPromote && promotions.canBePromoted()) {
val pos = thisCombatant.getTile().position
civ.addNotification("[${thisCombatant.unit.displayName()}] can be promoted!",
listOf(MapUnitAction(pos), PromoteUnitAction(thisCombatant.getName(), pos)),
NotificationCategory.Units, thisCombatant.unit.name)
}
}
private fun conquerCity(city: City, attacker: MapUnitCombatant) {

View File

@ -6,6 +6,9 @@ import com.unciv.logic.IsPartOfGameInfoSerialization
import com.unciv.logic.automation.Automation
import com.unciv.logic.automation.city.ConstructionAutomation
import com.unciv.logic.civilization.AlertType
import com.unciv.logic.civilization.CivilopediaAction
import com.unciv.logic.civilization.LocationAction
import com.unciv.logic.civilization.MapUnitAction
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.PopupAlert
@ -14,6 +17,7 @@ import com.unciv.logic.multiplayer.isUsersTurn
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.IConstruction
import com.unciv.models.ruleset.INonPerpetualConstruction
import com.unciv.models.ruleset.IRulesetObject
import com.unciv.models.ruleset.PerpetualConstruction
import com.unciv.models.ruleset.RejectionReasonType
import com.unciv.models.ruleset.Ruleset
@ -447,33 +451,40 @@ class CityConstructions : IsPartOfGameInfoSerialization {
validateConstructionQueue() // if we've build e.g. the Great Lighthouse, then Lighthouse is no longer relevant in the queue
construction as IRulesetObject // Always OK for INonPerpetualConstruction, but compiler doesn't know
val buildingIcon = "BuildingIcons/${construction.name}"
val pediaAction = CivilopediaAction(construction.makeLink())
val locationAction = if (construction is BaseUnit) MapUnitAction(city.location)
else LocationAction(city.location)
val locationAndPediaActions = listOf(locationAction, pediaAction)
if (construction is Building && construction.isWonder) {
city.civ.popupAlerts.add(PopupAlert(AlertType.WonderBuilt, construction.name))
for (civ in city.civ.gameInfo.civilizations) {
if (civ.hasExplored(city.getCenterTile()))
civ.addNotification("[${construction.name}] has been built in [${city.name}]", city.location,
civ.addNotification("[${construction.name}] has been built in [${city.name}]",
locationAndPediaActions,
if (civ == city.civ) NotificationCategory.Production else NotificationCategory.General, buildingIcon)
else
civ.addNotification("[${construction.name}] has been built in a faraway land", NotificationCategory.General, buildingIcon)
civ.addNotification("[${construction.name}] has been built in a faraway land",
pediaAction, NotificationCategory.General, buildingIcon)
}
} else {
val icon = if (construction is Building) buildingIcon else construction.name // could be a unit, in which case take the unit name.
city.civ.addNotification(
"[${construction.name}] has been built in [${city.name}]",
city.location, NotificationCategory.Production, NotificationIcon.Construction, icon)
locationAndPediaActions, NotificationCategory.Production, NotificationIcon.Construction, icon)
}
if (construction is Building && construction.hasUnique(UniqueType.TriggersAlertOnCompletion,
StateForConditionals(city.civ, city)
)) {
if (construction.hasUnique(UniqueType.TriggersAlertOnCompletion, StateForConditionals(city.civ, city))) {
for (otherCiv in city.civ.gameInfo.civilizations) {
// No need to notify ourself, since we already got the building notification anyway
if (otherCiv == city.civ) continue
val completingCivDescription =
if (otherCiv.knows(city.civ)) "[${city.civ.civName}]" else "An unknown civilization"
otherCiv.addNotification("$completingCivDescription has completed [${construction.name}]!",
NotificationCategory.General, NotificationIcon.Construction, buildingIcon)
pediaAction, NotificationCategory.General, NotificationIcon.Construction, buildingIcon)
}
}
return true

View File

@ -3,10 +3,14 @@ package com.unciv.logic.city.managers
import com.unciv.logic.city.City
import com.unciv.logic.city.CityFlags
import com.unciv.logic.city.CityFocus
import com.unciv.logic.civilization.CityAction
import com.unciv.logic.civilization.LocationAction
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.OverviewAction
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.screens.overviewscreen.EmpireOverviewCategories
import kotlin.math.min
import kotlin.random.Random
@ -50,7 +54,7 @@ class CityTurnManager(val city: City) {
city.setFlag(CityFlags.WeLoveTheKing, 20 + 1) // +1 because it will be decremented by 1 in the same startTurn()
city.civ.addNotification(
"Because they have [${city.demandedResource}], the citizens of [${city.name}] are celebrating We Love The King Day!",
city.location, NotificationCategory.General, NotificationIcon.City, NotificationIcon.Happiness)
CityAction.withLocation(city), NotificationCategory.General, NotificationIcon.City, NotificationIcon.Happiness)
}
}
@ -70,14 +74,14 @@ class CityTurnManager(val city: City) {
CityFlags.WeLoveTheKing.name -> {
city.civ.addNotification(
"We Love The King Day in [${city.name}] has ended.",
city.location, NotificationCategory.General, NotificationIcon.City)
CityAction.withLocation(city), NotificationCategory.General, NotificationIcon.City)
demandNewResource()
}
CityFlags.Resistance.name -> {
city.updateCitizens = true
city.civ.addNotification(
"The resistance in [${city.name}] has ended!",
city.location, NotificationCategory.General, "StatIcons/Resistance")
CityAction.withLocation(city), NotificationCategory.General, "StatIcons/Resistance")
}
}
}
@ -105,7 +109,8 @@ class CityTurnManager(val city: City) {
city.setFlag(CityFlags.ResourceDemand, 15 + Random.Default.nextInt(10))
else
city.civ.addNotification("[${city.name}] demands [${city.demandedResource}]!",
city.location, NotificationCategory.General, NotificationIcon.City, "ResourceIcons/${city.demandedResource}")
listOf(LocationAction(city.location), OverviewAction(EmpireOverviewCategories.Resources)),
NotificationCategory.General, NotificationIcon.City, "ResourceIcons/${city.demandedResource}")
}
@ -144,5 +149,4 @@ class CityTurnManager(val city: City) {
}
}
}

View File

@ -91,6 +91,10 @@ open class Notification() : IsPartOfGameInfoSerialization {
index = ++index % actions.size // cycle through tiles
}
fun resetExecuteRoundRobin() {
index = 0
}
/**
* Custom [Gdx.Json][Json] serializer/deserializer for one [Notification].
*
@ -111,7 +115,7 @@ open class Notification() : IsPartOfGameInfoSerialization {
companion object {
/** The switch that starts Phase III and dies with Phase V
* @see Serializer */
private const val compatibilityMode = true
private const val compatibilityMode = false
}
override fun write(json: Json, notification: Notification, knownType: Class<*>?) {

View File

@ -4,11 +4,13 @@ import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.utils.Json
import com.badlogic.gdx.utils.JsonValue
import com.unciv.logic.IsPartOfGameInfoSerialization
import com.unciv.logic.city.City
import com.unciv.ui.components.MayaCalendar
import com.unciv.ui.screens.cityscreen.CityScreen
import com.unciv.ui.screens.civilopediascreen.CivilopediaCategories
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen
import com.unciv.ui.screens.diplomacyscreen.DiplomacyScreen
import com.unciv.ui.screens.overviewscreen.EmpireOverviewCategories
import com.unciv.ui.screens.overviewscreen.EmpireOverviewScreen
import com.unciv.ui.screens.pickerscreens.PromotionPickerScreen
import com.unciv.ui.screens.pickerscreens.TechPickerScreen
import com.unciv.ui.screens.worldscreen.WorldScreen
@ -68,6 +70,9 @@ class CityAction(private val city: Vector2 = Vector2.Zero): NotificationAction {
if (cityObject.civ == worldScreen.viewingCiv)
worldScreen.game.pushScreen(CityScreen(cityObject))
}
companion object {
fun withLocation(city: City) = listOf(LocationAction(city.location), CityAction(city.location))
}
}
/** enter diplomacy screen */
@ -90,12 +95,17 @@ class MapUnitAction(private val location: Vector2 = Vector2.Zero) : Notification
override fun execute(worldScreen: WorldScreen) {
worldScreen.mapHolder.setCenterPosition(location, selectUnit = true)
}
companion object {
// Convenience shortcut as it makes replacing LocationAction calls easier (see above)
operator fun invoke(locations: Iterable<Vector2>): Sequence<MapUnitAction> =
locations.asSequence().map { MapUnitAction(it) }
}
}
/** A notification action that shows the Civilopedia entry for a Wonder. */
class WonderAction(private val wonderName: String = "") : NotificationAction {
/** A notification action that shows a Civilopedia entry, e.g. for a Wonder. */
class CivilopediaAction(private val link: String = "") : NotificationAction {
override fun execute(worldScreen: WorldScreen) {
worldScreen.game.pushScreen(CivilopediaScreen(worldScreen.gameInfo.ruleset, CivilopediaCategories.Wonder, wonderName))
worldScreen.game.pushScreen(CivilopediaScreen(worldScreen.gameInfo.ruleset, link = link))
}
}
@ -109,6 +119,16 @@ class PromoteUnitAction(private val name: String = "", private val location: Vec
}
}
/** Open the Empire Overview to a specific page, potentially "selecting" some entry */
class OverviewAction(
private val page: EmpireOverviewCategories = EmpireOverviewCategories.Resources,
private val select: String = ""
) : NotificationAction {
override fun execute(worldScreen: WorldScreen) {
worldScreen.game.pushScreen(EmpireOverviewScreen(worldScreen.selectedCiv, page, select))
}
}
@Suppress("PrivatePropertyName") // These names *must* match their class name, see below
internal class NotificationActionsDeserializer {
/* This exists as trick to leverage readFields for Json deserialization.
@ -127,14 +147,15 @@ internal class NotificationActionsDeserializer {
private val DiplomacyAction: DiplomacyAction? = null
private val MayaLongCountAction: MayaLongCountAction? = null
private val MapUnitAction: MapUnitAction? = null
private val WonderAction: WonderAction? = null
private val CivilopediaAction: CivilopediaAction? = null
private val PromoteUnitAction: PromoteUnitAction? = null
private val OverviewAction: OverviewAction? = null
fun read(json: Json, jsonData: JsonValue): List<NotificationAction> {
json.readFields(this, jsonData)
return listOfNotNull(
LocationAction, TechAction, CityAction, DiplomacyAction,
MayaLongCountAction, MapUnitAction, WonderAction, PromoteUnitAction
LocationAction, TechAction, CityAction, DiplomacyAction, MayaLongCountAction,
MapUnitAction, CivilopediaAction, PromoteUnitAction, OverviewAction
)
}
}

View File

@ -8,6 +8,7 @@ import com.unciv.logic.civilization.CivFlags
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.DiplomacyAction
import com.unciv.logic.civilization.LocationAction
import com.unciv.logic.civilization.MapUnitAction
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.PlayerType
@ -115,12 +116,12 @@ class CityStateFunctions(val civInfo: Civilization) {
placedUnit.promotions.XP += unique.params[0].toInt()
}
// Point to the places mentioned in the message _in that order_ (debatable)
val placedLocation = placedUnit.getTile().position
val locations = LocationAction(placedLocation, cities.city2.location, city.location)
// Point to the gifted unit, then to the other places mentioned in the message
val unitAction = sequenceOf(MapUnitAction(placedUnit.getTile().position))
val notificationActions = unitAction + LocationAction(cities.city2.location, city.location)
receivingCiv.addNotification(
"[${civInfo.civName}] gave us a [${militaryUnit.name}] as gift near [${city.name}]!",
locations,
notificationActions,
NotificationCategory.Units,
civInfo.civName,
militaryUnit.name
@ -228,18 +229,11 @@ class CityStateFunctions(val civInfo: Civilization) {
val oldAllyName = civInfo.getAllyCiv()
civInfo.setAllyCiv(newAllyName)
// If the city-state is captured by a civ, it stops being the ally of the civ it was previously an ally of.
// This means that it will NOT HAVE a capital at that time, so if we run getCapital we'll get a crash!
val capitalLocation = if (civInfo.cities.isNotEmpty() && civInfo.getCapital() != null) civInfo.getCapital()!!.location else null
if (newAllyName != null) {
val newAllyCiv = civInfo.gameInfo.getCivilization(newAllyName)
val text = "We have allied with [${civInfo.civName}]."
if (capitalLocation != null) newAllyCiv.addNotification(text, capitalLocation,
NotificationCategory.Diplomacy, civInfo.civName,
NotificationIcon.Diplomacy
)
else newAllyCiv.addNotification(text,
newAllyCiv.addNotification(text,
getNotificationActions(),
NotificationCategory.Diplomacy, civInfo.civName,
NotificationIcon.Diplomacy
)
@ -262,11 +256,8 @@ class CityStateFunctions(val civInfo: Civilization) {
if (oldAllyName != null && civInfo.isAlive()) {
val oldAllyCiv = civInfo.gameInfo.getCivilization(oldAllyName)
val text = "We have lost alliance with [${civInfo.civName}]."
if (capitalLocation != null) oldAllyCiv.addNotification(text, capitalLocation,
NotificationCategory.Diplomacy, civInfo.civName,
NotificationIcon.Diplomacy
)
else oldAllyCiv.addNotification(text,
oldAllyCiv.addNotification(text,
getNotificationActions(),
NotificationCategory.Diplomacy, civInfo.civName,
NotificationIcon.Diplomacy
)
@ -276,6 +267,21 @@ class CityStateFunctions(val civInfo: Civilization) {
}
}
/** @return a Sequence of NotificationActions for use in addNotification, showing Capital on map if any, then opening diplomacy */
fun getNotificationActions() = sequence {
// Notification click will first point to CS location, if any, then open diplomacy.
// That's fine for the influence notifications and for afraid too.
//
// If the city-state is captured by a civ, it stops being the ally of the civ it was previously an ally of.
// This means that it will NOT HAVE a capital at that time, so if we run getCapital()!! we'll get a crash!
// Or, City States can get stuck with only their Settler and no cities until late into a game if city placements are rare
// We also had `cities.asSequence() // in practice 0 or 1 entries, that's OK` before (a CS *can* have >1 cities but it will always raze conquests).
val capital = civInfo.getCapital()
if (capital != null)
yield(LocationAction(capital.location))
yield(DiplomacyAction(civInfo.civName))
}
fun getDiplomaticMarriageCost(): Int {
// https://github.com/Gedemon/Civ5-DLL/blob/master/CvGameCoreDLL_Expansion1/CvMinorCivAI.cpp, line 7812
var cost = (500 * civInfo.gameInfo.speed.goldCostModifier).toInt()

View File

@ -5,6 +5,8 @@ import com.unciv.Constants
import com.unciv.logic.IsPartOfGameInfoSerialization
import com.unciv.logic.civilization.AlertType
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.DiplomacyAction
import com.unciv.logic.civilization.LocationAction
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.PopupAlert
@ -527,18 +529,15 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
}
if (!civInfo.isDefeated()) { // don't display city state relationship notifications when the city state is currently defeated
val civCapitalLocation = if (civInfo.cities.any() && civInfo.getCapital() != null) civInfo.getCapital()!!.location else null
val notificationActions = civInfo.cityStateFunctions.getNotificationActions()
if (getTurnsToRelationshipChange() == 1) {
val text = "Your relationship with [${civInfo.civName}] is about to degrade"
if (civCapitalLocation != null) otherCiv().addNotification(text,
civCapitalLocation, NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy)
else otherCiv().addNotification(text, NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy)
otherCiv().addNotification(text, notificationActions, NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy)
}
if (initialRelationshipLevel >= RelationshipLevel.Friend && initialRelationshipLevel != relationshipIgnoreAfraid()) {
val text = "Your relationship with [${civInfo.civName}] degraded"
if (civCapitalLocation != null) otherCiv().addNotification(text, civCapitalLocation, NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy)
else otherCiv().addNotification(text, NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy)
otherCiv().addNotification(text, notificationActions, NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy)
}
// Potentially notify about afraid status
@ -549,8 +548,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
) {
setFlag(DiplomacyFlags.NotifiedAfraid, 20) // Wait 20 turns until next reminder
val text = "[${civInfo.civName}] is afraid of your military power!"
if (civCapitalLocation != null) otherCiv().addNotification(text, civCapitalLocation, NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy)
else otherCiv().addNotification(text, NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy)
otherCiv().addNotification(text, notificationActions, NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy)
}
}
}

View File

@ -3,6 +3,7 @@ package com.unciv.logic.civilization.managers
import com.unciv.logic.IsPartOfGameInfoSerialization
import com.unciv.logic.civilization.AlertType
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.CivilopediaAction
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.PopupAlert
import com.unciv.models.ruleset.unique.UniqueTriggerActivation
@ -44,7 +45,9 @@ class GoldenAgeManager : IsPartOfGameInfoSerialization {
fun enterGoldenAge(unmodifiedNumberOfTurns: Int = 10) {
turnsLeftForCurrentGoldenAge += calculateGoldenAgeLength(unmodifiedNumberOfTurns)
civInfo.addNotification("You have entered a Golden Age!", NotificationCategory.General, "StatIcons/Happiness")
civInfo.addNotification("You have entered a Golden Age!",
CivilopediaAction("Tutorial/Golden Age"),
NotificationCategory.General, "StatIcons/Happiness")
civInfo.popupAlerts.add(PopupAlert(AlertType.GoldenAge, ""))
for (unique in civInfo.getTriggeredUniques(UniqueType.TriggerUponEnteringGoldenAge))

View File

@ -1,3 +1,5 @@
@file:Suppress("ConvertArgumentToSet") // Flags all assignedQuests.removeAll(List) - not worth it
package com.unciv.logic.civilization.managers
import com.badlogic.gdx.math.Vector2
@ -8,6 +10,9 @@ import com.unciv.logic.IsPartOfGameInfoSerialization
import com.unciv.logic.civilization.CivFlags
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.DiplomacyAction
import com.unciv.logic.civilization.LocationAction
import com.unciv.logic.civilization.Notification // for Kdoc
import com.unciv.logic.civilization.NotificationAction
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.PlayerType
@ -30,7 +35,6 @@ import com.unciv.ui.components.extensions.toPercent
import kotlin.math.max
import kotlin.random.Random
@Suppress("NON_EXHAUSTIVE_WHEN") // Many when uses in here are much clearer this way
class QuestManager : IsPartOfGameInfoSerialization {
companion object {
@ -81,10 +85,8 @@ class QuestManager : IsPartOfGameInfoSerialization {
/** Returns the influence multiplier for [donor] from a Investment quest that [civInfo] might have (assumes only one) */
fun getInvestmentMultiplier(donor: String): Float {
val investmentQuest = assignedQuests.firstOrNull { it.questName == QuestName.Invest.value && it.assignee == donor }
return if (investmentQuest == null)
1f
else
investmentQuest.data1.toPercent()
?: return 1f
return investmentQuest.data1.toPercent()
}
fun clone(): QuestManager {
@ -311,12 +313,14 @@ class QuestManager : IsPartOfGameInfoSerialization {
var data1 = ""
var data2 = ""
var notificationActions: List<NotificationAction> = listOf(DiplomacyAction(civInfo.civName))
when (quest.name) {
QuestName.ClearBarbarianCamp.value -> {
val camp = getBarbarianEncampmentForQuest()!!
data1 = camp.position.x.toInt().toString()
data2 = camp.position.y.toInt().toString()
notificationActions = listOf(LocationAction(camp.position), notificationActions.first())
}
QuestName.ConnectResource.value -> data1 = getResourceForQuest(assignee)!!.name
QuestName.ConstructWonder.value -> data1 = getWonderToBuildForQuest(assignee)!!.name
@ -328,8 +332,10 @@ class QuestManager : IsPartOfGameInfoSerialization {
QuestName.PledgeToProtect.value -> data1 = getMostRecentBully()!!
QuestName.GiveGold.value -> data1 = getMostRecentBully()!!
QuestName.DenounceCiv.value -> data1 = getMostRecentBully()!!
QuestName.SpreadReligion.value -> { data1 = playerReligion!!.getReligionDisplayName() // For display
data2 = playerReligion.name } // To check completion
QuestName.SpreadReligion.value -> {
data1 = playerReligion!!.getReligionDisplayName() // For display
data2 = playerReligion.name // To check completion
}
QuestName.ContestCulture.value -> data1 = assignee.totalCultureForContests.toString()
QuestName.ContestFaith.value -> data1 = assignee.totalFaithForContests.toString()
QuestName.ContestTech.value -> data1 = assignee.tech.getNumberOfTechsResearched().toString()
@ -348,7 +354,7 @@ class QuestManager : IsPartOfGameInfoSerialization {
assignedQuests.add(newQuest)
assignee.addNotification("[${civInfo.civName}] assigned you a new quest: [${quest.name}].",
DiplomacyAction(civInfo.civName),
notificationActions,
NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest")
if (quest.isIndividual())
@ -553,20 +559,23 @@ class QuestManager : IsPartOfGameInfoSerialization {
val unitsToKill = max(3, totalMilitaryUnits / 4)
unitsToKillForCiv[attacker.civName] = unitsToKill
val location = if (civInfo.cities.isEmpty() || civInfo.getCapital() == null) null
else civInfo.getCapital()!!.location
// Ask for assistance
for (thirdCiv in civInfo.getKnownCivs().filter { it.isAlive() && !it.isAtWarWith(civInfo) && it.isMajorCiv() }) {
if (location != null)
thirdCiv.addNotification("[${civInfo.civName}] is being attacked by [${attacker.civName}]! Kill [$unitsToKill] of the attacker's military units and they will be immensely grateful.",
location, NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest")
else thirdCiv.addNotification("[${civInfo.civName}] is being attacked by [${attacker.civName}]! Kill [$unitsToKill] of the attacker's military units and they will be immensely grateful.",
NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest")
val location = civInfo.getCapital(firstCityIfNoCapital = true)?.location
for (thirdCiv in civInfo.getKnownCivs()) {
if (!thirdCiv.isMajorCiv() || thirdCiv.isDefeated() || thirdCiv.isAtWarWith(civInfo))
continue
notifyAskForAssistance(thirdCiv, attacker.civName, unitsToKill, location)
}
}
private fun notifyAskForAssistance(assignee: Civilization, attackerName: String, unitsToKill: Int, location: Vector2?) {
if (attackerName == assignee.civName) return // No "Hey Bob help us against Bob"
val message = "[${civInfo.civName}] is being attacked by [$attackerName]!" +
"Kill [$unitsToKill] of the attacker's military units and they will be immensely grateful."
// Note: that LocationAction pseudo-constructor is able to filter out null location(s), no need for `if`
assignee.addNotification(message, LocationAction(location), NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest")
}
/** Gets notified when [killed]'s military unit was killed by [killer], for war with major pseudo-quest */
fun militaryUnitKilledBy(killer: Civilization, killed: Civilization) {
if (!warWithMajorActive(killed)) return
@ -593,16 +602,10 @@ class QuestManager : IsPartOfGameInfoSerialization {
/** Called when a major civ meets the city-state for the first time. Mainly for war with major pseudo-quest. */
fun justMet(otherCiv: Civilization) {
val location = if (civInfo.cities.isEmpty() || civInfo.getCapital() == null) null
else civInfo.getCapital()!!.location
for ((attackerName, unitsToKill) in unitsToKillForCiv) {
if (location != null)
otherCiv.addNotification("[${civInfo.civName}] is being attacked by [$attackerName]! Kill [$unitsToKill] of the attacker's military units and they will be immensely grateful.",
location, NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest")
else otherCiv.addNotification("[${civInfo.civName}] is being attacked by [$attackerName]! Kill [$unitsToKill] of the attacker's military units and they will be immensely grateful.",
NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest")
}
if (unitsToKillForCiv.isEmpty()) return
val location = civInfo.getCapital(firstCityIfNoCapital = true)?.location
for ((attackerName, unitsToKill) in unitsToKillForCiv)
notifyAskForAssistance(otherCiv, attackerName, unitsToKill, location)
}
/** Ends War with Major pseudo-quests that aren't relevant any longer */

View File

@ -1,6 +1,7 @@
package com.unciv.models.ruleset
import com.unciv.models.stats.INamed
import com.unciv.logic.civilization.Civilization // for Kdoc
enum class QuestName(val value: String) {
Route("Route"),
@ -29,6 +30,10 @@ enum class QuestType {
}
/** [Quest] class holds all functionality relative to a quest */
// Notes: This is **not** `IsPartOfGameInfoSerialization`, only Ruleset.
// Saves contain [QuestManager]s instead, which contain lists of [AssignedQuest] instances.
// These are matched to this Quest **by name**.
// Note [name] must match one of the [QuestName] _values_ above for the Quest to have any functionality.
class Quest : INamed {
/** Unique identifier name of the quest, it is also shown */
@ -46,7 +51,7 @@ class Quest : INamed {
/** Maximum number of turns to complete the quest, 0 if there's no turn limit */
var duration: Int = 0
/** Minimum number of [CivInfo] needed to start the quest. It is meaningful only for [QuestType.Global]
/** Minimum number of [Civilization]s needed to start the quest. It is meaningful only for [QuestType.Global]
* quests [type]. */
var minimumCivs: Int = 1
@ -55,7 +60,7 @@ class Quest : INamed {
* Both are mapped here as 'how much to multiply the weight of this quest for this kind of city-state' */
var weightForCityStateType = HashMap<String, Float>()
/** Checks if [this] is a Global quest */
/** Checks if `this` is a Global quest */
fun isGlobal(): Boolean = type == QuestType.Global
fun isIndividual(): Boolean = !isGlobal()
}

View File

@ -7,6 +7,7 @@ import com.unciv.logic.city.City
import com.unciv.logic.civilization.CivFlags
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.LocationAction
import com.unciv.logic.civilization.MapUnitAction
import com.unciv.logic.civilization.MayaLongCountAction
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.NotificationIcon
@ -65,7 +66,7 @@ object UniqueTriggerActivation {
val placedUnit = if (city != null || tile == null)
civInfo.units.addUnit(unitName, chosenCity) ?: return false
else civInfo.units.placeUnitNearTile(tile!!.position, unitName) ?: return false
else civInfo.units.placeUnitNearTile(tile.position, unitName) ?: return false
val notificationText = getNotificationText(notification, triggerNotificationText,
"Gained [1] [$unitName] unit(s)")
@ -73,7 +74,7 @@ object UniqueTriggerActivation {
civInfo.addNotification(
notificationText,
placedUnit.getTile().position,
MapUnitAction(placedUnit.getTile().position),
NotificationCategory.Units,
placedUnit.name
)
@ -97,7 +98,7 @@ object UniqueTriggerActivation {
val tilesUnitsWerePlacedOn: MutableList<Vector2> = mutableListOf()
repeat(actualAmount) {
val placedUnit = if (city != null || tile == null) civInfo.units.addUnit(unitName, chosenCity)
else civInfo.units.placeUnitNearTile(tile!!.position, unitName)
else civInfo.units.placeUnitNearTile(tile.position, unitName)
if (placedUnit != null)
tilesUnitsWerePlacedOn.add(placedUnit.getTile().position)
}
@ -109,7 +110,7 @@ object UniqueTriggerActivation {
civInfo.addNotification(
notificationText,
LocationAction(tilesUnitsWerePlacedOn),
MapUnitAction(tilesUnitsWerePlacedOn),
NotificationCategory.Units,
civInfo.getEquivalentUnit(unit).name
)
@ -118,8 +119,11 @@ object UniqueTriggerActivation {
UniqueType.OneTimeFreeUnitRuins -> {
var unit = civInfo.getEquivalentUnit(unique.params[0])
if ( unit.hasUnique(UniqueType.FoundCity) && civInfo.isOneCityChallenger()) {
val replacementUnit = ruleSet.units.values.firstOrNull{it.getMatchingUniques(UniqueType.BuildImprovements)
.any { it.params[0] == "Land" }} ?: return false
val replacementUnit = ruleSet.units.values
.firstOrNull {
it.getMatchingUniques(UniqueType.BuildImprovements)
.any { unique -> unique.params[0] == "Land" }
} ?: return false
unit = civInfo.getEquivalentUnit(replacementUnit.name)
}
@ -134,7 +138,10 @@ object UniqueTriggerActivation {
else notification
civInfo.addNotification(
notificationText,
LocationAction(placedUnit.getTile().position, tile?.position),
sequence {
yield(MapUnitAction(placedUnit.getTile().position))
yieldAll(LocationAction(tile?.position))
},
NotificationCategory.Units,
placedUnit.name
)
@ -393,7 +400,7 @@ object UniqueTriggerActivation {
if (notification != null) {
civInfo.addNotification(
notification,
LocationAction(promotedUnitLocations),
MapUnitAction(promotedUnitLocations),
NotificationCategory.Units,
"unitPromotionIcons/${unique.params[1]}"
)
@ -411,10 +418,10 @@ object UniqueTriggerActivation {
* The very first time after acquiring this policy, the timer is set to half of its normal value
* This is the basics, and apart from this, there is some randomness in the exact turn count, but I don't know how much
* There is surprisingly little information findable online about this policy, and the civ 5 source files are
* also quite though to search through, so this might all be incorrect.
* also quite tough to search through, so this might all be incorrect.
* For now this mechanic seems decent enough that this is fine.
* Note that the way this is implemented now, this unique does NOT stack
* I could parametrize the [Allied], but eh.
* I could parametrize the 'Allied' of the Unique text, but eh.
*/
UniqueType.CityStateCanGiftGreatPeople -> {
civInfo.addFlag(
@ -643,7 +650,7 @@ object UniqueTriggerActivation {
return false
}
fun getNotificationText(notification: String?, triggerNotificationText: String?, effectNotificationText:String):String?{
private fun getNotificationText(notification: String?, triggerNotificationText: String?, effectNotificationText: String): String? {
return if (!notification.isNullOrEmpty()) notification
else if (triggerNotificationText != null)
{
@ -659,7 +666,7 @@ object UniqueTriggerActivation {
unique: Unique,
unit: MapUnit,
notification: String? = null,
triggerNotificationText:String? = null
triggerNotificationText: String? = null
): Boolean {
when (unique.type) {
UniqueType.OneTimeUnitHeal -> {

View File

@ -36,6 +36,7 @@ abstract class EmpireOverviewTab (
val worldScreen = GUI.getWorldScreen()
worldScreen.notificationsScroll.oneTimeNotification = notification
UncivGame.Current.resetToWorldScreen()
notification.resetExecuteRoundRobin()
notification.execute(worldScreen)
}
}

View File

@ -41,6 +41,8 @@ class EspionageOverviewScreen(val civInfo: Civilization) : PickerScreen(true) {
private var moveSpyHereButtons = hashMapOf<Button, City?>()
init {
spySelectionTable.defaults().pad(10f)
citySelectionTable.defaults().pad(5f)
middlePanes.add(spyScrollPane)
middlePanes.addSeparatorVertical()
middlePanes.add(cityScrollPane)
@ -64,12 +66,12 @@ class EspionageOverviewScreen(val civInfo: Civilization) : PickerScreen(true) {
private fun updateSpyList() {
spySelectionTable.clear()
spySelectionTable.add("Spy".toLabel()).pad(10f)
spySelectionTable.add("Location".toLabel()).pad(10f)
spySelectionTable.add("Action".toLabel()).pad(10f).row()
spySelectionTable.add("Spy".toLabel())
spySelectionTable.add("Location".toLabel())
spySelectionTable.add("Action".toLabel()).row()
for (spy in civInfo.espionageManager.spyList) {
spySelectionTable.add(spy.name.toLabel()).pad(10f)
spySelectionTable.add(spy.getLocationName().toLabel()).pad(10f)
spySelectionTable.add(spy.name.toLabel())
spySelectionTable.add(spy.getLocationName().toLabel())
val actionString =
when (spy.action) {
SpyAction.None, SpyAction.StealingTech, SpyAction.Surveillance -> spy.action.displayString
@ -77,7 +79,7 @@ class EspionageOverviewScreen(val civInfo: Civilization) : PickerScreen(true) {
SpyAction.RiggingElections -> TODO()
SpyAction.CounterIntelligence -> TODO()
}
spySelectionTable.add(actionString.toLabel()).pad(10f)
spySelectionTable.add(actionString.toLabel())
val moveSpyButton = "Move".toTextButton()
moveSpyButton.onClick {
@ -89,36 +91,33 @@ class EspionageOverviewScreen(val civInfo: Civilization) : PickerScreen(true) {
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 and their hideout
// 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
for ((button, city) in moveSpyHereButtons) {
// For now, only allow spies to be sent to cities of other major civs and their hideout
// Not own cities as counterintelligence isn't implemented
// Not city-state civs as rigging elections isn't implemented
button.isVisible = city == null // hideout
|| (city.civ.isMajorCiv()
&& city.civ != civInfo
&& !city.espionage.hasSpyOf(civInfo)
)
) {
button.isVisible = true
}
}
}
spySelectionTable.add(moveSpyButton).pad(5f).row()
spySelectionTable.add(moveSpyButton).pad(5f, 10f, 5f, 20f).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()
citySelectionTable.add()
citySelectionTable.add("City".toLabel())
citySelectionTable.add("Spy present".toLabel()).row()
// First add the hideout to the table
citySelectionTable.add().pad(5f)
citySelectionTable.add("Spy Hideout".toLabel()).pad(5f)
citySelectionTable.add().pad(5f)
citySelectionTable.add()
citySelectionTable.add("Spy Hideout".toLabel())
citySelectionTable.add()
val moveSpyHereButton = getMoveToCityButton(null)
citySelectionTable.add(moveSpyHereButton).row()
@ -143,21 +142,22 @@ class EspionageOverviewScreen(val civInfo: Civilization) : PickerScreen(true) {
}
private fun addCityToSelectionTable(city: City) {
citySelectionTable.add(ImageGetter.getNationPortrait(city.civ.nation, 30f)).pad(5f)
citySelectionTable.add(city.name.toLabel()).pad(5f)
citySelectionTable.add(ImageGetter.getNationPortrait(city.civ.nation, 30f))
.padLeft(20f)
citySelectionTable.add(city.name.toLabel(hideIcons = true))
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)
citySelectionTable.add()
}
val moveSpyHereButton = getMoveToCityButton(city)
citySelectionTable.add(moveSpyHereButton).pad(5f)
citySelectionTable.add(moveSpyHereButton)
citySelectionTable.row()
}

View File

@ -99,7 +99,7 @@ class ResourcesOverviewTab(
gameInfo.getExploredResourcesNotification(viewingPlayer, name)
) }
}
private fun TileResource.getLabel() = name.toLabel().apply {
private fun TileResource.getLabel() = name.toLabel(hideIcons = true).apply {
onClick {
overviewScreen.game.pushScreen(CivilopediaScreen(gameInfo.ruleset, CivilopediaCategories.Resource, this@getLabel.name))
}

View File

@ -126,8 +126,8 @@ class AlertPopup(
player.getDiplomacyManager(bullyOrAttacker).sideWithCityState()
}.row()
addCloseButton("Very well.", KeyboardBinding.Cancel) {
val capitalLocation = LocationAction(cityState.cities.asSequence().map { it.location }) // in practice 0 or 1 entries, that's OK
player.addNotification("You have broken your Pledge to Protect [${cityState.civName}]!", capitalLocation, NotificationCategory.Diplomacy, cityState.civName)
player.addNotification("You have broken your Pledge to Protect [${cityState.civName}]!",
cityState.cityStateFunctions.getNotificationActions(), NotificationCategory.Diplomacy, cityState.civName)
cityState.cityStateFunctions.removeProtectorCiv(player, forced = true)
}.row()
}