Competition quests in progress display tied leaders (and your place if you're behind) (#11224)

* Minor Quest/QuestManager linting

* Cache Quest and QuestName references and use them

* Nicer randomWeighted and fix UniqueType.ResourceWeighting

* Integrate @soggerr's #10739 - show tied leaders and your position if you aren't leading

* One more lint
This commit is contained in:
SomeTroglodyte 2024-03-05 22:04:40 +01:00 committed by GitHub
parent 31931d3849
commit d5fda541ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 377 additions and 277 deletions

View File

@ -170,8 +170,10 @@ Ally = Allié
[questName] (+[influenceAmount] influence) = [questName] (+[influenceAmount] influence)
[remainingTurns] turns remaining = [remainingTurns] tours restants
Current leader is [civInfo] with [amount] [stat] generated. = [civInfo] est actuellement en tête et a généré [amount] [stat].
Current leader is [civInfo] with [amount] Technologies discovered. = [civInfo] est actuellement en tête avec [amount] Technologies découvertes.
Current leader(s): [leaders] = Actuellement en tête: [leaders]
Current leader(s): [leaders], you: [yourScore] = Ton résultat: [yourScore] est dépassé par: [leaders]
# In the two templates above, 'leaders' and 'yourScore' will use the following:
[civilizations] with [value] [valueType] = [civilizations] avec [value] [valueType]
Demands = Demandes
Please don't settle new cities near us. = Veuillez ne pas fonder de villes près de nous.

View File

@ -170,8 +170,10 @@ Ally =
[questName] (+[influenceAmount] influence) =
[remainingTurns] turns remaining =
Current leader is [civInfo] with [amount] [stat] generated. =
Current leader is [civInfo] with [amount] Technologies discovered. =
Current leader(s): [leaders] =
Current leader(s): [leaders], you: [yourScore] =
# In the two templates above, 'leaders' will be one or more of the following, and 'yourScore' one:
[civInfo] with [value] [valueType] =
Demands =
Please don't settle new cities near us. =

View File

@ -30,9 +30,9 @@ import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.translations.fillPlaceholders
import com.unciv.models.translations.getPlaceholderParameters
import com.unciv.models.translations.tr
import com.unciv.ui.components.extensions.randomWeighted
import com.unciv.ui.components.extensions.toPercent
import kotlin.math.max
import kotlin.random.Random
class QuestManager : IsPartOfGameInfoSerialization {
@ -58,10 +58,13 @@ class QuestManager : IsPartOfGameInfoSerialization {
/** Civilization object holding and dispatching quests */
@Transient
lateinit var civInfo: Civilization
private lateinit var civ: Civilization
/** Readability helper to access the Ruleset through [civ] */
private val ruleset get() = civ.gameInfo.ruleset
/** List of active quests, both global and individual ones*/
var assignedQuests: ArrayList<AssignedQuest> = ArrayList()
private var assignedQuests: ArrayList<AssignedQuest> = ArrayList()
/** Number of turns left before starting new global quest */
private var globalQuestCountdown: Int = UNSET
@ -76,15 +79,24 @@ class QuestManager : IsPartOfGameInfoSerialization {
/** For this attacker, number of units killed by each civ */
private var unitsKilledFromCiv: HashMap<String, HashMap<String, Int>> = HashMap()
/** Returns true if [civInfo] have active quests for [challenger] */
fun haveQuestsFor(challenger: Civilization): Boolean = assignedQuests.any { it.assignee == challenger.civName }
/** Returns true if [civ] have active quests for [challenger] */
fun haveQuestsFor(challenger: Civilization): Boolean = getAssignedQuestsFor(challenger.civName).any()
/** Returns true if [civInfo] has asked anyone to conquer [target] */
fun wantsDead(target: String): Boolean = assignedQuests.any { it.questName == QuestName.ConquerCityState.value && it.data1 == target }
/** Access all assigned Quests for [civName] */
fun getAssignedQuestsFor(civName: String) =
assignedQuests.asSequence().filter { it.assignee == civName }
/** Returns the influence multiplier for [donor] from a Investment quest that [civInfo] might have (assumes only one) */
/** Access all assigned Quests of "type" [questName] */
// Note if we decide to cache an index of these (such as `assignedQuests.groupBy { it.questNameInstance }`), this accessor would simplify the transition
private fun getAssignedQuestsOfName(questName: QuestName) =
assignedQuests.asSequence().filter { it.questNameInstance == questName }
/** Returns true if [civ] has asked anyone to conquer [target] */
fun wantsDead(target: String): Boolean = getAssignedQuestsOfName(QuestName.ConquerCityState).any { it.data1 == target }
/** Returns the influence multiplier for [donor] from a Investment quest that [civ] might have (assumes only one) */
fun getInvestmentMultiplier(donor: String): Float {
val investmentQuest = assignedQuests.firstOrNull { it.questName == QuestName.Invest.value && it.assignee == donor }
val investmentQuest = getAssignedQuestsOfName(QuestName.Invest).firstOrNull { it.assignee == donor }
?: return 1f
return investmentQuest.data1.toPercent()
}
@ -96,31 +108,30 @@ class QuestManager : IsPartOfGameInfoSerialization {
toReturn.assignedQuests.addAll(assignedQuests)
toReturn.unitsToKillForCiv.putAll(unitsToKillForCiv)
for ((attacker, unitsKilled) in unitsKilledFromCiv) {
toReturn.unitsKilledFromCiv[attacker] = HashMap()
toReturn.unitsKilledFromCiv[attacker]!!.putAll(unitsKilled)
toReturn.unitsKilledFromCiv[attacker] = HashMap(unitsKilled)
}
return toReturn
}
fun setTransients(civInfo: Civilization) {
this.civInfo = civInfo
fun setTransients(civ: Civilization) {
this.civ = civ
for (quest in assignedQuests)
quest.gameInfo = civInfo.gameInfo
quest.setTransients(civ.gameInfo)
}
fun endTurn() {
if (civInfo.isDefeated()) {
if (civ.isDefeated()) {
assignedQuests.clear()
individualQuestCountdown.clear()
globalQuestCountdown = UNSET
return
}
if (civInfo.cities.none()) return // don't assign quests until we have a city
if (civ.cities.isEmpty()) return // don't assign quests until we have a city
seedGlobalQuestCountdown()
seedIndividualQuestsCountdown()
seedIndividualQuestsCountdowns()
decrementQuestCountdowns()
@ -144,26 +155,26 @@ class QuestManager : IsPartOfGameInfoSerialization {
}
private fun seedGlobalQuestCountdown() {
if (civInfo.gameInfo.turns < GLOBAL_QUEST_FIRST_POSSIBLE_TURN)
if (civ.gameInfo.turns < GLOBAL_QUEST_FIRST_POSSIBLE_TURN)
return
if (globalQuestCountdown != UNSET)
return
val countdown =
if (civInfo.gameInfo.turns == GLOBAL_QUEST_FIRST_POSSIBLE_TURN)
if (civ.gameInfo.turns == GLOBAL_QUEST_FIRST_POSSIBLE_TURN)
Random.nextInt(GLOBAL_QUEST_FIRST_POSSIBLE_TURN_RAND)
else
GLOBAL_QUEST_MIN_TURNS_BETWEEN + Random.nextInt(GLOBAL_QUEST_RAND_TURNS_BETWEEN)
globalQuestCountdown = (countdown * civInfo.gameInfo.speed.modifier).toInt()
globalQuestCountdown = (countdown * civ.gameInfo.speed.modifier).toInt()
}
private fun seedIndividualQuestsCountdown() {
if (civInfo.gameInfo.turns < INDIVIDUAL_QUEST_FIRST_POSSIBLE_TURN)
private fun seedIndividualQuestsCountdowns() {
if (civ.gameInfo.turns < INDIVIDUAL_QUEST_FIRST_POSSIBLE_TURN)
return
val majorCivs = civInfo.gameInfo.getAliveMajorCivs()
val majorCivs = civ.gameInfo.getAliveMajorCivs()
for (majorCiv in majorCivs)
if (!individualQuestCountdown.containsKey(majorCiv.civName) || individualQuestCountdown[majorCiv.civName] == UNSET)
seedIndividualQuestsCountdown(majorCiv)
@ -171,36 +182,34 @@ class QuestManager : IsPartOfGameInfoSerialization {
private fun seedIndividualQuestsCountdown(challenger: Civilization) {
val countdown: Int =
if (civInfo.gameInfo.turns == INDIVIDUAL_QUEST_FIRST_POSSIBLE_TURN)
if (civ.gameInfo.turns == INDIVIDUAL_QUEST_FIRST_POSSIBLE_TURN)
Random.nextInt(INDIVIDUAL_QUEST_FIRST_POSSIBLE_TURN_RAND)
else
INDIVIDUAL_QUEST_MIN_TURNS_BETWEEN + Random.nextInt(
INDIVIDUAL_QUEST_RAND_TURNS_BETWEEN
)
individualQuestCountdown[challenger.civName] = (countdown * civInfo.gameInfo.speed.modifier).toInt()
individualQuestCountdown[challenger.civName] = (countdown * civ.gameInfo.speed.modifier).toInt()
}
// Readabilty helper - No asSequence(): call frequency * data size is small
private fun getQuests(predicate: (Quest) -> Boolean) = ruleset.quests.values.filter(predicate)
private fun tryStartNewGlobalQuest() {
if (globalQuestCountdown != 0)
return
if (assignedQuests.count { it.isGlobal() } >= GLOBAL_QUEST_MAX_ACTIVE)
return
val globalQuests = civInfo.gameInfo.ruleset.quests.values.filter { it.isGlobal() }
val majorCivs = civInfo.getKnownCivs().filter { it.isMajorCiv() && !it.isAtWarWith(civInfo) }
val assignableQuests = ArrayList<Quest>()
for (quest in globalQuests) {
val numberValidMajorCivs = majorCivs.count { civ -> isQuestValid(quest, civ) }
if (numberValidMajorCivs >= quest.minimumCivs)
assignableQuests.add(quest)
val majorCivs = civ.getKnownCivs().filter { it.isMajorCiv() && !it.isAtWarWith(civ) } // A Sequence - fine because the count below can be different for each Quest
fun Quest.isAssignable() = majorCivs.count { civ -> isQuestValid(this, civ) } >= minimumCivs
val assignableQuests = getQuests {
it.isGlobal() && it.isAssignable()
}
val weights = assignableQuests.map { getQuestWeight(it.name) }
if (assignableQuests.isNotEmpty()) {
val quest = assignableQuests.randomWeighted(weights)
val assignees = civInfo.gameInfo.getAliveMajorCivs().filter { !it.isAtWarWith(civInfo) && isQuestValid(quest, it) }
val quest = assignableQuests.randomWeighted { getQuestWeight(it.name) }
val assignees = civ.gameInfo.getAliveMajorCivs().filter { !it.isAtWarWith(civ) && isQuestValid(quest, it) }
assignNewQuest(quest, assignees)
globalQuestCountdown = UNSET
@ -209,19 +218,18 @@ class QuestManager : IsPartOfGameInfoSerialization {
private fun tryStartNewIndividualQuests() {
for ((challengerName, countdown) in individualQuestCountdown) {
val challenger = civInfo.gameInfo.getCivilization(challengerName)
val challenger = civ.gameInfo.getCivilization(challengerName)
if (countdown != 0)
continue
if (assignedQuests.count { it.assignee == challenger.civName && it.isIndividual() } >= INDIVIDUAL_QUEST_MAX_ACTIVE)
if (getAssignedQuestsFor(challenger.civName).count { it.isIndividual() } >= INDIVIDUAL_QUEST_MAX_ACTIVE)
continue
val assignableQuests = civInfo.gameInfo.ruleset.quests.values.filter { it.isIndividual() && isQuestValid(it, challenger) }
val weights = assignableQuests.map { getQuestWeight(it.name) }
val assignableQuests = getQuests { it.isIndividual() && isQuestValid(it, challenger) }
if (assignableQuests.isNotEmpty()) {
val quest = assignableQuests.randomWeighted(weights)
val quest = assignableQuests.randomWeighted { getQuestWeight(it.name) }
val assignees = arrayListOf(challenger)
assignNewQuest(quest, assignees)
@ -230,46 +238,42 @@ class QuestManager : IsPartOfGameInfoSerialization {
}
private fun tryBarbarianInvasion() {
if ((civInfo.getTurnsTillCallForBarbHelp() == null || civInfo.getTurnsTillCallForBarbHelp() == 0)
&& civInfo.cityStateFunctions.getNumThreateningBarbarians() >= 2) {
if ((civ.getTurnsTillCallForBarbHelp() == null || civ.getTurnsTillCallForBarbHelp() == 0)
&& civ.cityStateFunctions.getNumThreateningBarbarians() >= 2) {
for (otherCiv in civInfo.getKnownCivs().filter {
for (otherCiv in civ.getKnownCivs().filter {
it.isMajorCiv()
&& it.isAlive()
&& !it.isAtWarWith(civInfo)
&& it.getProximity(civInfo) <= Proximity.Far
&& !it.isAtWarWith(civ)
&& it.getProximity(civ) <= Proximity.Far
}) {
otherCiv.addNotification("[${civInfo.civName}] is being invaded by Barbarians! Destroy Barbarians near their territory to earn Influence.",
civInfo.getCapital()!!.location,
NotificationCategory.Diplomacy, civInfo.civName,
otherCiv.addNotification(
"[${civ.civName}] is being invaded by Barbarians! Destroy Barbarians near their territory to earn Influence.",
civ.getCapital()!!.location,
NotificationCategory.Diplomacy, civ.civName,
NotificationIcon.War
)
}
civInfo.addFlag(CivFlags.TurnsTillCallForBarbHelp.name, 30)
civ.addFlag(CivFlags.TurnsTillCallForBarbHelp.name, 30)
}
}
private fun handleGlobalQuests() {
// Remove any participants that are no longer valid because of being dead or at war with the CS
assignedQuests.removeAll { it.isGlobal() &&
!canAssignAQuestTo(civInfo.gameInfo.getCivilization(it.assignee)) }
val globalQuestsExpired = assignedQuests.filter { it.isGlobal() && it.isExpired() }.map { it.questName }.distinct()
!canAssignAQuestTo(civ.gameInfo.getCivilization(it.assignee)) }
val globalQuestsExpired = assignedQuests.filter { it.isGlobal() && it.isExpired() }.map { it.questNameInstance }.distinct()
for (globalQuestName in globalQuestsExpired)
handleGlobalQuest(globalQuestName)
}
private fun handleGlobalQuest(questName: String) {
val quests = assignedQuests.filter { it.questName == questName }
if (quests.isEmpty())
return
private fun handleGlobalQuest(questName: QuestName) {
val winnersAndLosers = WinnersAndLosers(questName)
winnersAndLosers.winners.forEach { giveReward(it) }
winnersAndLosers.losers.forEach { notifyExpired(it, winnersAndLosers.winners) }
val topScore = quests.maxOf { getScoreForQuest(it) }
val winners = quests.filter { getScoreForQuest(it) == topScore }
winners.forEach { giveReward(it) }
for (loser in quests.filterNot { it in winners })
notifyExpired(loser, winners)
assignedQuests.removeAll(quests)
assignedQuests.removeAll(winnersAndLosers.winners)
assignedQuests.removeAll(winnersAndLosers.losers)
}
private fun handleIndividualQuests() {
@ -279,7 +283,7 @@ class QuestManager : IsPartOfGameInfoSerialization {
/** If quest is complete, it gives the influence reward to the player.
* Returns true if the quest can be removed (is either complete, obsolete or expired) */
private fun handleIndividualQuest(assignedQuest: AssignedQuest): Boolean {
val assignee = civInfo.gameInfo.getCivilization(assignedQuest.assignee)
val assignee = civ.gameInfo.getCivilization(assignedQuest.assignee)
// One of the civs is defeated, or they started a war: remove quest
if (!canAssignAQuestTo(assignee))
@ -305,195 +309,269 @@ class QuestManager : IsPartOfGameInfoSerialization {
private fun assignNewQuest(quest: Quest, assignees: Iterable<Civilization>) {
val turn = civInfo.gameInfo.turns
val turn = civ.gameInfo.turns
for (assignee in assignees) {
val playerReligion = civInfo.gameInfo.religions.values.firstOrNull { it.foundingCivName == assignee.civName && it.isMajorReligion() }
var data1 = ""
var data2 = ""
var notificationActions: List<NotificationAction> = listOf(DiplomacyAction(civInfo.civName))
var notificationActions: List<NotificationAction> = listOf(DiplomacyAction(civ.civName))
when (quest.name) {
QuestName.ClearBarbarianCamp.value -> {
when (quest.questNameInstance) {
QuestName.ClearBarbarianCamp -> {
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
QuestName.GreatPerson.value -> data1 = getGreatPersonForQuest(assignee)!!.name
QuestName.FindPlayer.value -> data1 = getCivilizationToFindForQuest(assignee)!!.civName
QuestName.FindNaturalWonder.value -> data1 = getNaturalWonderToFindForQuest(assignee)!!
QuestName.ConquerCityState.value -> data1 = getCityStateTarget(assignee)!!.civName
QuestName.BullyCityState.value -> data1 = getCityStateTarget(assignee)!!.civName
QuestName.PledgeToProtect.value -> data1 = getMostRecentBully()!!
QuestName.GiveGold.value -> data1 = getMostRecentBully()!!
QuestName.DenounceCiv.value -> data1 = getMostRecentBully()!!
QuestName.SpreadReligion.value -> {
data1 = playerReligion!!.getReligionDisplayName() // For display
QuestName.ConnectResource -> data1 = getResourceForQuest(assignee)!!.name
QuestName.ConstructWonder -> data1 = getWonderToBuildForQuest(assignee)!!.name
QuestName.GreatPerson -> data1 = getGreatPersonForQuest(assignee)!!.name
QuestName.FindPlayer -> data1 = getCivilizationToFindForQuest(assignee)!!.civName
QuestName.FindNaturalWonder -> data1 = getNaturalWonderToFindForQuest(assignee)!!
QuestName.ConquerCityState -> data1 = getCityStateTarget(assignee)!!.civName
QuestName.BullyCityState -> data1 = getCityStateTarget(assignee)!!.civName
QuestName.PledgeToProtect -> data1 = getMostRecentBully()!!
QuestName.GiveGold -> data1 = getMostRecentBully()!!
QuestName.DenounceCiv -> data1 = getMostRecentBully()!!
QuestName.SpreadReligion -> {
val playerReligion = civ.gameInfo.religions.values
.first { it.foundingCivName == assignee.civName && it.isMajorReligion() } // isQuestValid must have ensured this won't throw
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()
QuestName.Invest.value -> data1 = quest.description.getPlaceholderParameters().first()
QuestName.ContestCulture -> data1 = assignee.totalCultureForContests.toString()
QuestName.ContestFaith -> data1 = assignee.totalFaithForContests.toString()
QuestName.ContestTech -> data1 = assignee.tech.getNumberOfTechsResearched().toString()
QuestName.Invest -> data1 = quest.description.getPlaceholderParameters().first()
else -> Unit
}
val newQuest = AssignedQuest(
questName = quest.name,
assigner = civInfo.civName,
assigner = civ.civName,
assignee = assignee.civName,
assignedOnTurn = turn,
data1 = data1,
data2 = data2
)
newQuest.gameInfo = civInfo.gameInfo
newQuest.setTransients(civ.gameInfo, quest)
assignedQuests.add(newQuest)
assignee.addNotification("[${civInfo.civName}] assigned you a new quest: [${quest.name}].",
notificationActions,
NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest")
if (quest.isIndividual())
individualQuestCountdown[assignee.civName] = UNSET
assignee.addNotification("[${civ.civName}] assigned you a new quest: [${quest.name}].",
notificationActions,
NotificationCategory.Diplomacy, civ.civName, "OtherIcons/Quest")
}
}
/** Returns true if [civInfo] can assign a quest to [challenger] */
/** Returns true if [civ] can assign a quest to [challenger] */
private fun canAssignAQuestTo(challenger: Civilization): Boolean {
return !challenger.isDefeated() && challenger.isMajorCiv() &&
civInfo.knows(challenger) && !civInfo.isAtWarWith(challenger)
civ.knows(challenger) && !civ.isAtWarWith(challenger)
}
/** Returns true if the [quest] can be assigned to [challenger] */
private fun isQuestValid(quest: Quest, challenger: Civilization): Boolean {
if (!canAssignAQuestTo(challenger))
return false
if (assignedQuests.any { it.assignee == challenger.civName && it.questName == quest.name })
if (getAssignedQuestsOfName(quest.questNameInstance).any { it.assignee == challenger.civName })
return false
if (quest.isIndividual() && civInfo.getDiplomacyManager(challenger).hasFlag(DiplomacyFlags.Bullied))
if (quest.isIndividual() && civ.getDiplomacyManager(challenger).hasFlag(DiplomacyFlags.Bullied))
return false
val mostRecentBully = getMostRecentBully()
val playerReligion = civInfo.gameInfo.religions.values.firstOrNull { it.foundingCivName == challenger.civName && it.isMajorReligion() }?.name
return when (quest.name) {
QuestName.ClearBarbarianCamp.value -> getBarbarianEncampmentForQuest() != null
QuestName.Route.value -> !challenger.cities.none()
&& !challenger.isCapitalConnectedToCity(civInfo.getCapital()!!)
// Need to have a city within 7 tiles on the same continent
&& challenger.cities.any { it.getCenterTile().aerialDistanceTo(civInfo.getCapital()!!.getCenterTile()) <= 7
&& it.getCenterTile().getContinent() == civInfo.getCapital()!!.getCenterTile().getContinent() }
QuestName.ConnectResource.value -> getResourceForQuest(challenger) != null
QuestName.ConstructWonder.value -> getWonderToBuildForQuest(challenger) != null
QuestName.GreatPerson.value -> getGreatPersonForQuest(challenger) != null
QuestName.FindPlayer.value -> getCivilizationToFindForQuest(challenger) != null
QuestName.FindNaturalWonder.value -> getNaturalWonderToFindForQuest(challenger) != null
QuestName.PledgeToProtect.value -> mostRecentBully != null && challenger !in civInfo.cityStateFunctions.getProtectorCivs()
QuestName.GiveGold.value -> mostRecentBully != null
QuestName.DenounceCiv.value -> mostRecentBully != null && challenger.knows(mostRecentBully)
&& !challenger.getDiplomacyManager(mostRecentBully).hasFlag(DiplomacyFlags.Denunciation)
&& challenger.getDiplomacyManager(mostRecentBully).diplomaticStatus != DiplomaticStatus.War
&& !( challenger.playerType == PlayerType.Human && civInfo.gameInfo.getCivilization(mostRecentBully).playerType == PlayerType.Human)
QuestName.SpreadReligion.value -> playerReligion != null && civInfo.getCapital()!!.religion.getMajorityReligion()?.name != playerReligion
QuestName.ConquerCityState.value -> getCityStateTarget(challenger) != null && civInfo.cityStatePersonality != CityStatePersonality.Friendly
QuestName.BullyCityState.value -> getCityStateTarget(challenger) != null
QuestName.ContestFaith.value -> civInfo.gameInfo.isReligionEnabled()
return when (quest.questNameInstance) {
QuestName.ClearBarbarianCamp -> getBarbarianEncampmentForQuest() != null
QuestName.Route -> isRouteQuestValid(challenger)
QuestName.ConnectResource -> getResourceForQuest(challenger) != null
QuestName.ConstructWonder -> getWonderToBuildForQuest(challenger) != null
QuestName.GreatPerson -> getGreatPersonForQuest(challenger) != null
QuestName.FindPlayer -> getCivilizationToFindForQuest(challenger) != null
QuestName.FindNaturalWonder -> getNaturalWonderToFindForQuest(challenger) != null
QuestName.PledgeToProtect -> getMostRecentBully() != null && challenger !in civ.cityStateFunctions.getProtectorCivs()
QuestName.GiveGold -> getMostRecentBully() != null
QuestName.DenounceCiv -> isDenounceCivQuestValid(challenger, getMostRecentBully())
QuestName.SpreadReligion -> {
val playerReligion = civ.gameInfo.religions.values.firstOrNull { it.foundingCivName == challenger.civName && it.isMajorReligion() }?.name
playerReligion != null && civ.getCapital()!!.religion.getMajorityReligion()?.name != playerReligion
}
QuestName.ConquerCityState -> getCityStateTarget(challenger) != null && civ.cityStatePersonality != CityStatePersonality.Friendly
QuestName.BullyCityState -> getCityStateTarget(challenger) != null
QuestName.ContestFaith -> civ.gameInfo.isReligionEnabled()
else -> true
}
}
private fun isRouteQuestValid(challenger: Civilization): Boolean {
if (challenger.cities.isEmpty()) return false
if (challenger.isCapitalConnectedToCity(civ.getCapital()!!)) return false
val capital = civ.getCapital() ?: return false
val capitalTile = capital.getCenterTile()
return challenger.cities.any {
it.getCenterTile().getContinent() == capitalTile.getContinent() &&
it.getCenterTile().aerialDistanceTo(capitalTile) <= 7
}
}
private fun isDenounceCivQuestValid(challenger: Civilization, mostRecentBully: String?): Boolean {
return mostRecentBully != null
&& challenger.knows(mostRecentBully)
&& !challenger.getDiplomacyManager(mostRecentBully).hasFlag(DiplomacyFlags.Denunciation)
&& challenger.getDiplomacyManager(mostRecentBully).diplomaticStatus != DiplomaticStatus.War
&& !( challenger.playerType == PlayerType.Human
&& civ.gameInfo.getCivilization(mostRecentBully).playerType == PlayerType.Human)
}
/** Returns true if the [assignedQuest] is successfully completed */
private fun isComplete(assignedQuest: AssignedQuest): Boolean {
val assignee = civInfo.gameInfo.getCivilization(assignedQuest.assignee)
return when (assignedQuest.questName) {
QuestName.Route.value -> assignee.isCapitalConnectedToCity(civInfo.getCapital()!!)
QuestName.ConnectResource.value -> assignee.detailedCivResources.map { it.resource }.contains(civInfo.gameInfo.ruleset.tileResources[assignedQuest.data1])
QuestName.ConstructWonder.value -> assignee.cities.any { it.cityConstructions.isBuilt(assignedQuest.data1) }
QuestName.GreatPerson.value -> assignee.units.getCivGreatPeople().any { it.baseUnit.getReplacedUnit(civInfo.gameInfo.ruleset).name == assignedQuest.data1 }
QuestName.FindPlayer.value -> assignee.hasMetCivTerritory(civInfo.gameInfo.getCivilization(assignedQuest.data1))
QuestName.FindNaturalWonder.value -> assignee.naturalWonders.contains(assignedQuest.data1)
QuestName.PledgeToProtect.value -> assignee in civInfo.cityStateFunctions.getProtectorCivs()
QuestName.DenounceCiv.value -> assignee.getDiplomacyManager(assignedQuest.data1).hasFlag(DiplomacyFlags.Denunciation)
QuestName.SpreadReligion.value -> civInfo.getCapital()!!.religion.getMajorityReligion() == civInfo.gameInfo.religions[assignedQuest.data2]
val assignee = civ.gameInfo.getCivilization(assignedQuest.assignee)
return when (assignedQuest.questNameInstance) {
QuestName.Route -> assignee.isCapitalConnectedToCity(civ.getCapital()!!)
QuestName.ConnectResource -> assignee.detailedCivResources.map { it.resource }.contains(ruleset.tileResources[assignedQuest.data1])
QuestName.ConstructWonder -> assignee.cities.any { it.cityConstructions.isBuilt(assignedQuest.data1) }
QuestName.GreatPerson -> assignee.units.getCivGreatPeople().any { it.baseUnit.getReplacedUnit(ruleset).name == assignedQuest.data1 }
QuestName.FindPlayer -> assignee.hasMetCivTerritory(civ.gameInfo.getCivilization(assignedQuest.data1))
QuestName.FindNaturalWonder -> assignee.naturalWonders.contains(assignedQuest.data1)
QuestName.PledgeToProtect -> assignee in civ.cityStateFunctions.getProtectorCivs()
QuestName.DenounceCiv -> assignee.getDiplomacyManager(assignedQuest.data1).hasFlag(DiplomacyFlags.Denunciation)
QuestName.SpreadReligion -> civ.getCapital()!!.religion.getMajorityReligion() == civ.gameInfo.religions[assignedQuest.data2]
else -> false
}
}
/** Returns true if the [assignedQuest] request cannot be fulfilled anymore */
private fun isObsolete(assignedQuest: AssignedQuest): Boolean {
val assignee = civInfo.gameInfo.getCivilization(assignedQuest.assignee)
return when (assignedQuest.questName) {
QuestName.ClearBarbarianCamp.value -> civInfo.gameInfo.tileMap[assignedQuest.data1.toInt(), assignedQuest.data2.toInt()].improvement != Constants.barbarianEncampment
QuestName.ConstructWonder.value -> civInfo.gameInfo.getCities().any { it.civ != assignee && it.cityConstructions.isBuilt(assignedQuest.data1) }
QuestName.FindPlayer.value -> civInfo.gameInfo.getCivilization(assignedQuest.data1).isDefeated()
QuestName.ConquerCityState.value -> civInfo.gameInfo.getCivilization(assignedQuest.data1).isDefeated()
QuestName.BullyCityState.value -> civInfo.gameInfo.getCivilization(assignedQuest.data1).isDefeated()
QuestName.DenounceCiv.value -> civInfo.gameInfo.getCivilization(assignedQuest.data1).isDefeated()
val assignee = civ.gameInfo.getCivilization(assignedQuest.assignee)
return when (assignedQuest.questNameInstance) {
QuestName.ClearBarbarianCamp -> civ.gameInfo.tileMap[assignedQuest.data1.toInt(), assignedQuest.data2.toInt()].improvement != Constants.barbarianEncampment
QuestName.ConstructWonder -> civ.gameInfo.getCities().any { it.civ != assignee && it.cityConstructions.isBuilt(assignedQuest.data1) }
QuestName.FindPlayer -> civ.gameInfo.getCivilization(assignedQuest.data1).isDefeated()
QuestName.ConquerCityState -> civ.gameInfo.getCivilization(assignedQuest.data1).isDefeated()
QuestName.BullyCityState -> civ.gameInfo.getCivilization(assignedQuest.data1).isDefeated()
QuestName.DenounceCiv -> civ.gameInfo.getCivilization(assignedQuest.data1).isDefeated()
else -> false
}
}
/** Increments [assignedQuest.assignee][AssignedQuest.assignee] influence on [civInfo] and adds a [Notification] */
/** Increments [assignedQuest.assignee][AssignedQuest.assignee] influence on [civ] and adds a [Notification] */
private fun giveReward(assignedQuest: AssignedQuest) {
val rewardInfluence = civInfo.gameInfo.ruleset.quests[assignedQuest.questName]!!.influence
val assignee = civInfo.gameInfo.getCivilization(assignedQuest.assignee)
val rewardInfluence = assignedQuest.getInfluence()
val assignee = civ.gameInfo.getCivilization(assignedQuest.assignee)
civInfo.getDiplomacyManager(assignedQuest.assignee).addInfluence(rewardInfluence)
civ.getDiplomacyManager(assignedQuest.assignee).addInfluence(rewardInfluence)
if (rewardInfluence > 0)
assignee.addNotification(
"[${civInfo.civName}] rewarded you with [${rewardInfluence.toInt()}] influence for completing the [${assignedQuest.questName}] quest.",
civInfo.getCapital()!!.location, NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest"
"[${civ.civName}] rewarded you with [${rewardInfluence.toInt()}] influence for completing the [${assignedQuest.questName}] quest.",
civ.getCapital()!!.location, NotificationCategory.Diplomacy, civ.civName, "OtherIcons/Quest"
)
// We may have received bonuses from city-state friend-ness or ally-ness
for (city in civInfo.cities)
for (city in civ.cities)
city.cityStats.update()
}
/** Notifies the assignee of [assignedQuest] that the quest is now obsolete or expired.
* Optionally displays the [winners] of global quests. */
private fun notifyExpired(assignedQuest: AssignedQuest, winners: List<AssignedQuest> = emptyList()) {
val assignee = civInfo.gameInfo.getCivilization(assignedQuest.assignee)
val assignee = civ.gameInfo.getCivilization(assignedQuest.assignee)
if (winners.isEmpty()) {
assignee.addNotification(
"[${civInfo.civName}] no longer needs your help with the [${assignedQuest.questName}] quest.",
civInfo.getCapital()!!.location,
NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest")
"[${civ.civName}] no longer needs your help with the [${assignedQuest.questName}] quest.",
civ.getCapital()!!.location,
NotificationCategory.Diplomacy, civ.civName, "OtherIcons/Quest")
} else {
assignee.addNotification(
"The [${assignedQuest.questName}] quest for [${civInfo.civName}] has ended. It was won by [${winners.joinToString { "{${it.assignee}}" }}].",
civInfo.getCapital()!!.location,
NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest")
"The [${assignedQuest.questName}] quest for [${civ.civName}] has ended. It was won by [${winners.joinToString { "{${it.assignee}}" }}].",
civ.getCapital()!!.location,
NotificationCategory.Diplomacy, civ.civName, "OtherIcons/Quest")
}
}
/** Returns the score for the [assignedQuest] */
private fun getScoreForQuest(assignedQuest: AssignedQuest): Int {
val assignee = civInfo.gameInfo.getCivilization(assignedQuest.assignee)
return when (assignedQuest.questName) {
QuestName.ContestCulture.value -> assignee.totalCultureForContests - assignedQuest.data1.toInt()
QuestName.ContestFaith.value -> assignee.totalFaithForContests - assignedQuest.data1.toInt()
QuestName.ContestTech.value -> assignee.tech.getNumberOfTechsResearched() - assignedQuest.data1.toInt()
val assignee = civ.gameInfo.getCivilization(assignedQuest.assignee)
return when (assignedQuest.questNameInstance) {
//quest total = civ total - the value at the time the quest started (which was stored in assignedQuest.data1)
QuestName.ContestCulture -> assignee.totalCultureForContests - assignedQuest.data1.toInt()
QuestName.ContestFaith -> assignee.totalFaithForContests - assignedQuest.data1.toInt()
QuestName.ContestTech -> assignee.tech.getNumberOfTechsResearched() - assignedQuest.data1.toInt()
else -> 0
}
}
/** Returns a string with the leading civ and their score for [questName] */
fun getLeaderStringForQuest(questName: String): String {
val leadingQuest = assignedQuests.filter { it.questName == questName }.maxByOrNull { getScoreForQuest(it) }
?: return ""
/** Evaluate a contest-type quest:
*
* - Determines [winner(s)][winners] (as AssignedQuest instances, which name their assignee): Those whose score is the [maximum score][maxScore], possibly tied.
* and [losers]: all other [assignedQuests] matching parameter `questName`.
* - Called by the UI via [getScoreStringForGlobalQuest] before a Contest is resolved to display who currently leads,
* and by [handleGlobalQuest] to distribute rewards and notifications.
* @param questName filters [assignedQuests] by their [QuestName][AssignedQuest.questNameInstance]
*/
inner class WinnersAndLosers(questName: QuestName) {
val winners = mutableListOf<AssignedQuest>()
val losers = mutableListOf<AssignedQuest>()
var maxScore: Int = -1
private set
return when (questName) {
QuestName.ContestCulture.value -> "Current leader is [${leadingQuest.assignee}] with [${getScoreForQuest(leadingQuest)}] [Culture] generated."
QuestName.ContestFaith.value -> "Current leader is [${leadingQuest.assignee}] with [${getScoreForQuest(leadingQuest)}] [Faith] generated."
QuestName.ContestTech.value -> "Current leader is [${leadingQuest.assignee}] with [${getScoreForQuest(leadingQuest)}] Technologies discovered."
else -> ""
init {
require(ruleset.quests[questName.value]!!.isGlobal())
for (quest in getAssignedQuestsOfName(questName)) {
val qScore = getScoreForQuest(quest)
when {
qScore <= 0 -> Unit // no civ is a winner if their score is 0
qScore < maxScore ->
losers.add(quest)
qScore == maxScore ->
winners.add(quest)
else -> { // qScore > maxScore
losers.addAll(winners)
winners.clear()
winners.add(quest)
maxScore = qScore
}
}
}
}
}
/** Returns a string to show "competition" status:
* - Show leading civ(s) (more than one only if tied for first place) with best score.
* - The assignee civ of the given [inquiringAssignedQuest] is shown for comparison if it is not among the leaders.
*
* Assumes the result will be passed to [String.tr] - but parts are pretranslated to avoid nested brackets.
* Tied leaders are separated by ", " - translators cannot influence this, sorry.
* @param inquiringAssignedQuest Determines ["type"][AssignedQuest.questNameInstance] to find all competitors in [assignedQuests] and [viewing civ][AssignedQuest.assignee].
*/
fun getScoreStringForGlobalQuest(inquiringAssignedQuest: AssignedQuest): String {
require(inquiringAssignedQuest.assigner == civ.civName)
require(inquiringAssignedQuest.isGlobal())
val scoreDescriptor = when (inquiringAssignedQuest.questNameInstance) {
QuestName.ContestCulture -> "Culture"
QuestName.ContestFaith -> "Faith"
QuestName.ContestTech -> "Technologies"
else -> return "" //This handles global quests which aren't a competition, like invest
}
// Get list of leaders with leading score (the losers aren't used here)
val evaluation = WinnersAndLosers(inquiringAssignedQuest.questNameInstance)
if (evaluation.winners.isEmpty()) //Only show leaders if there are some
return ""
val listOfLeadersAsTranslatedString = evaluation.winners.joinToString(separator = ", ") { it.assignee.tr() }
fun getScoreString(name: String, score: Int) = "[$name] with [$score] [$scoreDescriptor]".tr()
val leadersString = getScoreString(listOfLeadersAsTranslatedString, evaluation.maxScore)
if (inquiringAssignedQuest in evaluation.winners)
return "Current leader(s): [$leadersString]"
val yourScoreString = getScoreString(inquiringAssignedQuest.assignee, getScoreForQuest(inquiringAssignedQuest))
return "Current leader(s): [$leadersString], you: [$yourScoreString]"
}
/**
* Gets notified a barbarian camp in [location] has been cleared by [civInfo].
@ -501,8 +579,7 @@ class QuestManager : IsPartOfGameInfoSerialization {
* multiple civilizations, so after this notification all matching quests are removed.
*/
fun barbarianCampCleared(civInfo: Civilization, location: Vector2) {
val matchingQuests = assignedQuests.asSequence()
.filter { it.questName == QuestName.ClearBarbarianCamp.value }
val matchingQuests = getAssignedQuestsOfName(QuestName.ClearBarbarianCamp)
.filter { it.data1.toInt() == location.x.toInt() && it.data2.toInt() == location.y.toInt() }
val winningQuest = matchingQuests.filter { it.assignee == civInfo.civName }.firstOrNull()
@ -516,8 +593,7 @@ class QuestManager : IsPartOfGameInfoSerialization {
* Gets notified the city state [cityState] was just conquered by [attacker].
*/
fun cityStateConquered(cityState: Civilization, attacker: Civilization) {
val matchingQuests = assignedQuests.asSequence()
.filter { it.questName == QuestName.ConquerCityState.value }
val matchingQuests = getAssignedQuestsOfName(QuestName.ConquerCityState)
.filter { it.data1 == cityState.civName && it.assignee == attacker.civName }
for (quest in matchingQuests)
@ -530,8 +606,7 @@ class QuestManager : IsPartOfGameInfoSerialization {
* Gets notified the city state [cityState] was just bullied by [bully].
*/
fun cityStateBullied(cityState: Civilization, bully: Civilization) {
val matchingQuests = assignedQuests.asSequence()
.filter { it.questName == QuestName.BullyCityState.value }
val matchingQuests = getAssignedQuestsOfName(QuestName.BullyCityState)
.filter { it.data1 == cityState.civName && it.assignee == bully.civName}
for (quest in matchingQuests)
@ -540,29 +615,30 @@ class QuestManager : IsPartOfGameInfoSerialization {
assignedQuests.removeAll(matchingQuests)
// What idiots haha oh wait that's us
if (civInfo == cityState) {
if (civ != cityState) return
// Revoke most quest types from the bully
val revokedQuests = assignedQuests.asSequence()
.filter { it.assignee == bully.civName && (it.isIndividual() || it.questName == QuestName.Invest.value) }
val revokedQuests = getAssignedQuestsFor(bully.civName)
.filter { it.isIndividual() || it.questNameInstance == QuestName.Invest }
.toList()
assignedQuests.removeAll(revokedQuests)
if (revokedQuests.count() > 0)
bully.addNotification("[${civInfo.civName}] cancelled the quests they had given you because you demanded tribute from them.",
DiplomacyAction(civInfo.civName),
NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest")
}
if (revokedQuests.isEmpty()) return
bully.addNotification("[${civ.civName}] cancelled the quests they had given you because you demanded tribute from them.",
DiplomacyAction(civ.civName),
NotificationCategory.Diplomacy, civ.civName, "OtherIcons/Quest")
}
/** Gets notified when we are attacked, for war with major pseudo-quest */
fun wasAttackedBy(attacker: Civilization) {
// Set target number units to kill
val totalMilitaryUnits = attacker.units.getCivUnits().count { !it.isCivilian() }
val unitsToKill = max(3, totalMilitaryUnits / 4)
val unitsToKill = (totalMilitaryUnits / 4).coerceAtMost(3)
unitsToKillForCiv[attacker.civName] = unitsToKill
// Ask for assistance
val location = civInfo.getCapital(firstCityIfNoCapital = true)?.location
for (thirdCiv in civInfo.getKnownCivs()) {
if (!thirdCiv.isMajorCiv() || thirdCiv.isDefeated() || thirdCiv.isAtWarWith(civInfo))
val location = civ.getCapital(firstCityIfNoCapital = true)?.location
for (thirdCiv in civ.getKnownCivs()) {
if (!thirdCiv.isMajorCiv() || thirdCiv.isDefeated() || thirdCiv.isAtWarWith(civ))
continue
notifyAskForAssistance(thirdCiv, attacker.civName, unitsToKill, location)
}
@ -570,11 +646,11 @@ class QuestManager : IsPartOfGameInfoSerialization {
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]!" +
val message = "[${civ.civName}] is being attacked by [$attackerName]!" +
// Space relevant in template!
" 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")
assignee.addNotification(message, LocationAction(location), NotificationCategory.Diplomacy, civ.civName, "OtherIcons/Quest")
}
/** Gets notified when [killed]'s military unit was killed by [killer], for war with major pseudo-quest */
@ -582,21 +658,20 @@ class QuestManager : IsPartOfGameInfoSerialization {
if (!warWithMajorActive(killed)) return
// No credit if we're at war or haven't met
if (!civInfo.knows(killer) || civInfo.isAtWarWith(killer)) return
if (!civ.knows(killer) || civ.isAtWarWith(killer)) return
// Make the map if we haven't already
if (unitsKilledFromCiv[killed.civName] == null)
unitsKilledFromCiv[killed.civName] = HashMap()
val unitsKilledFromCivEntry = unitsKilledFromCiv.getOrPut(killed.civName) { HashMap() }
// Update kill count
val updatedKillCount = 1 + (unitsKilledFromCiv[killed.civName]!![killer.civName] ?: 0)
unitsKilledFromCiv[killed.civName]!![killer.civName] = updatedKillCount
val updatedKillCount = 1 + (unitsKilledFromCivEntry[killer.civName] ?: 0)
unitsKilledFromCivEntry[killer.civName] = updatedKillCount
// Quest complete?
if (updatedKillCount >= unitsToKillForCiv[killed.civName]!!) {
killer.addNotification("[${civInfo.civName}] is deeply grateful for your assistance in the war against [${killed.civName}]!",
DiplomacyAction(civInfo.civName), NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest")
civInfo.getDiplomacyManager(killer).addInfluence(100f) // yikes
killer.addNotification("[${civ.civName}] is deeply grateful for your assistance in the war against [${killed.civName}]!",
DiplomacyAction(civ.civName), NotificationCategory.Diplomacy, civ.civName, "OtherIcons/Quest")
civ.getDiplomacyManager(killer).addInfluence(100f) // yikes
endWarWithMajorQuest(killed)
}
}
@ -604,28 +679,28 @@ 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) {
if (unitsToKillForCiv.isEmpty()) return
val location = civInfo.getCapital(firstCityIfNoCapital = true)?.location
val location = civ.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 */
private fun tryEndWarWithMajorQuests() {
for (attacker in unitsToKillForCiv.keys.map { civInfo.gameInfo.getCivilization(it) }) {
if (civInfo.isDefeated()
for (attacker in unitsToKillForCiv.keys.map { civ.gameInfo.getCivilization(it) }) {
if (civ.isDefeated()
|| attacker.isDefeated()
|| !civInfo.isAtWarWith(attacker)) {
|| !civ.isAtWarWith(attacker)) {
endWarWithMajorQuest(attacker)
}
}
}
private fun endWarWithMajorQuest(attacker: Civilization) {
for (thirdCiv in civInfo.getKnownCivs().filterNot { it.isDefeated() || it == attacker || it.isAtWarWith(civInfo) }) {
for (thirdCiv in civ.getKnownCivs().filterNot { it.isDefeated() || it == attacker || it.isAtWarWith(civ) }) {
if (unitsKilledSoFar(attacker, thirdCiv) >= unitsToKill(attacker)) // Don't show the notification to the one who won the quest
continue
thirdCiv.addNotification("[${civInfo.civName}] no longer needs your assistance against [${attacker.civName}].",
DiplomacyAction(civInfo.civName), NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest")
thirdCiv.addNotification("[${civ.civName}] no longer needs your assistance against [${attacker.civName}].",
DiplomacyAction(civ.civName), NotificationCategory.Diplomacy, civ.civName, "OtherIcons/Quest")
}
unitsToKillForCiv.remove(attacker.civName)
unitsKilledFromCiv.remove(attacker.civName)
@ -648,8 +723,7 @@ class QuestManager : IsPartOfGameInfoSerialization {
* Gets notified when given gold by [donorCiv].
*/
fun receivedGoldGift(donorCiv: Civilization) {
val matchingQuests = assignedQuests.asSequence()
.filter { it.questName == QuestName.GiveGold.value }
val matchingQuests = getAssignedQuestsOfName(QuestName.GiveGold)
.filter { it.assignee == donorCiv.civName }
for (quest in matchingQuests)
@ -663,23 +737,23 @@ class QuestManager : IsPartOfGameInfoSerialization {
*/
private fun getQuestWeight(questName: String): Float {
var weight = 1f
val quest = civInfo.gameInfo.ruleset.quests[questName] ?: return 0f
val quest = ruleset.quests[questName] ?: return 0f
val personalityWeight = quest.weightForCityStateType[civInfo.cityStatePersonality.name]
val personalityWeight = quest.weightForCityStateType[civ.cityStatePersonality.name]
if (personalityWeight != null) weight *= personalityWeight
val traitWeight = quest.weightForCityStateType[civInfo.cityStateType.name]
val traitWeight = quest.weightForCityStateType[civ.cityStateType.name]
if (traitWeight != null) weight *= traitWeight
return weight
}
//region get-quest-target
/**
* Returns a random [Tile] containing a Barbarian encampment within 8 tiles of [civInfo]
* Returns a random [Tile] containing a Barbarian encampment within 8 tiles of [civ]
* to be destroyed
*/
private fun getBarbarianEncampmentForQuest(): Tile? {
val encampments = civInfo.getCapital()!!.getCenterTile().getTilesInDistance(8)
val encampments = civ.getCapital()!!.getCenterTile().getTilesInDistance(8)
.filter { it.improvement == Constants.barbarianEncampment }.toList()
if (encampments.isNotEmpty())
@ -691,15 +765,15 @@ class QuestManager : IsPartOfGameInfoSerialization {
/**
* Returns a random resource to be connected to the [challenger]'s trade route as a quest.
* The resource must be a [ResourceType.Luxury] or [ResourceType.Strategic], must not be owned
* by the [civInfo] and the [challenger], and must be viewable by the [challenger];
* by the [civ] and the [challenger], and must be viewable by the [challenger];
* if none exists, it returns null.
*/
private fun getResourceForQuest(challenger: Civilization): TileResource? {
val ownedByCityStateResources = civInfo.detailedCivResources.map { it.resource }
val ownedByCityStateResources = civ.detailedCivResources.map { it.resource }
val ownedByMajorResources = challenger.detailedCivResources.map { it.resource }
val resourcesOnMap = civInfo.gameInfo.tileMap.values.asSequence().mapNotNull { it.resource }.distinct()
val viewableResourcesForChallenger = resourcesOnMap.map { civInfo.gameInfo.ruleset.tileResources[it]!! }
val resourcesOnMap = civ.gameInfo.tileMap.values.asSequence().mapNotNull { it.resource }.distinct()
val viewableResourcesForChallenger = resourcesOnMap.map { ruleset.tileResources[it]!! }
.filter { it.revealedBy == null || challenger.tech.isResearched(it.revealedBy!!) }
val notOwnedResources = viewableResourcesForChallenger.filter {
@ -715,18 +789,18 @@ class QuestManager : IsPartOfGameInfoSerialization {
}
private fun getWonderToBuildForQuest(challenger: Civilization): Building? {
val startingEra = civInfo.gameInfo.ruleset.eras[civInfo.gameInfo.gameParameters.startingEra]!!
val wonders = civInfo.gameInfo.ruleset.buildings.values
val startingEra = ruleset.eras[civ.gameInfo.gameParameters.startingEra]!!
val wonders = ruleset.buildings.values
.filter { building ->
// Buildable wonder
building.isWonder
&& challenger.tech.isResearched(building)
&& civInfo.gameInfo.getCities().none { it.cityConstructions.isBuilt(building.name) }
&& civ.gameInfo.getCities().none { it.cityConstructions.isBuilt(building.name) }
// Can't be disabled
&& building.name !in startingEra.startingObsoleteWonders
&& (civInfo.gameInfo.isReligionEnabled() || !building.hasUnique(UniqueType.HiddenWithoutReligion))
&& (civ.gameInfo.isReligionEnabled() || !building.hasUnique(UniqueType.HiddenWithoutReligion))
// Can't be more than 25% built anywhere
&& civInfo.gameInfo.getCities().none {
&& civ.gameInfo.getCities().none {
it.cityConstructions.getWorkDone(building.name) * 3 > it.cityConstructions.getRemainingWork(building.name) }
// Can't be a unique wonder
&& building.uniqueTo == null
@ -742,7 +816,7 @@ class QuestManager : IsPartOfGameInfoSerialization {
* Returns a random Natural Wonder not yet discovered by [challenger].
*/
private fun getNaturalWonderToFindForQuest(challenger: Civilization): String? {
val naturalWondersToFind = civInfo.gameInfo.tileMap.naturalWonders.subtract(challenger.naturalWonders)
val naturalWondersToFind = civ.gameInfo.tileMap.naturalWonders.subtract(challenger.naturalWonders)
if (naturalWondersToFind.isNotEmpty())
return naturalWondersToFind.random()
@ -751,20 +825,20 @@ class QuestManager : IsPartOfGameInfoSerialization {
}
/**
* Returns a Great Person [BaseUnit] that is not owned by both the [challenger] and the [civInfo]
* Returns a Great Person [BaseUnit] that is not owned by both the [challenger] and the [civ]
*/
private fun getGreatPersonForQuest(challenger: Civilization): BaseUnit? {
val ruleSet = civInfo.gameInfo.ruleset
val ruleset = ruleset // omit if the accessor should be converted to a transient field
val challengerGreatPeople = challenger.units.getCivGreatPeople().map { it.baseUnit.getReplacedUnit(ruleSet) }
val cityStateGreatPeople = civInfo.units.getCivGreatPeople().map { it.baseUnit.getReplacedUnit(ruleSet) }
val challengerGreatPeople = challenger.units.getCivGreatPeople().map { it.baseUnit.getReplacedUnit(ruleset) }
val cityStateGreatPeople = civ.units.getCivGreatPeople().map { it.baseUnit.getReplacedUnit(ruleset) }
val greatPeople = challenger.greatPeople.getGreatPeople()
.map { it.getReplacedUnit(ruleSet) }
.map { it.getReplacedUnit(ruleset) }
.distinct()
.filterNot { challengerGreatPeople.contains(it)
|| cityStateGreatPeople.contains(it)
|| (it.hasUnique(UniqueType.HiddenWithoutReligion) && !civInfo.gameInfo.isReligionEnabled()) }
|| (it.hasUnique(UniqueType.HiddenWithoutReligion) && !civ.gameInfo.isReligionEnabled()) }
.toList()
if (greatPeople.isNotEmpty())
@ -788,65 +862,77 @@ class QuestManager : IsPartOfGameInfoSerialization {
}
/**
* Returns a city-state [Civilization] that [civInfo] wants to target for hostile quests
* Returns a city-state [Civilization] that [civ] wants to target for hostile quests
*/
private fun getCityStateTarget(challenger: Civilization): Civilization? {
val closestProximity = civInfo.gameInfo.getAliveCityStates()
.mapNotNull { civInfo.proximity[it.civName] }.filter { it != Proximity.None }.minByOrNull { it.ordinal }
val closestProximity = civ.gameInfo.getAliveCityStates()
.mapNotNull { civ.proximity[it.civName] }.filter { it != Proximity.None }.minByOrNull { it.ordinal }
if (closestProximity == null || closestProximity == Proximity.Distant) // None close enough
return null
val validTargets = civInfo.getKnownCivs().filter { it.isCityState() && challenger.knows(it)
&& civInfo.proximity[it.civName] == closestProximity }
val validTargets = civ.getKnownCivs().filter { it.isCityState() && challenger.knows(it)
&& civ.proximity[it.civName] == closestProximity }
return validTargets.toList().randomOrNull()
}
/** Returns a [Civilization] of the civ that most recently bullied [civInfo].
/** Returns a [Civilization] of the civ that most recently bullied [civ].
* Note: forgets after 20 turns has passed! */
private fun getMostRecentBully(): String? {
val bullies = civInfo.diplomacy.values.filter { it.hasFlag(DiplomacyFlags.Bullied)}
val bullies = civ.diplomacy.values.filter { it.hasFlag(DiplomacyFlags.Bullied) }
return bullies.maxByOrNull { it.getFlag(DiplomacyFlags.Bullied) }?.otherCivName
}
//endregion
}
class AssignedQuest(val questName: String = "",
class AssignedQuest(
val questName: String = "",
val assigner: String = "",
val assignee: String = "",
val assignedOnTurn: Int = 0,
val data1: String = "",
val data2: String = "") : IsPartOfGameInfoSerialization {
val data2: String = ""
) : IsPartOfGameInfoSerialization {
@Transient
lateinit var gameInfo: GameInfo
private lateinit var gameInfo: GameInfo
fun isIndividual(): Boolean = !isGlobal()
fun isGlobal(): Boolean = gameInfo.ruleset.quests[questName]!!.isGlobal()
@Suppress("MemberVisibilityCanBePrivate")
fun doesExpire(): Boolean = gameInfo.ruleset.quests[questName]!!.duration > 0
fun isExpired(): Boolean = doesExpire() && getRemainingTurns() == 0
@Suppress("MemberVisibilityCanBePrivate")
fun getDuration(): Int = (gameInfo.speed.modifier * gameInfo.ruleset.quests[questName]!!.duration).toInt()
fun getRemainingTurns(): Int = max(0, (assignedOnTurn + getDuration()) - gameInfo.turns)
@Transient
private lateinit var questObject: Quest
fun getDescription(): String {
val quest = gameInfo.ruleset.quests[questName]!!
return quest.description.fillPlaceholders(data1)
val questNameInstance get() = questObject.questNameInstance
internal fun setTransients(gameInfo: GameInfo, quest: Quest? = null) {
this.gameInfo = gameInfo
questObject = quest ?: gameInfo.ruleset.quests[questName]!!
}
fun isIndividual(): Boolean = !isGlobal()
fun isGlobal(): Boolean = questObject.isGlobal()
@Suppress("MemberVisibilityCanBePrivate")
fun doesExpire(): Boolean = questObject.duration > 0
fun isExpired(): Boolean = doesExpire() && getRemainingTurns() == 0
@Suppress("MemberVisibilityCanBePrivate")
fun getDuration(): Int = (gameInfo.speed.modifier * questObject.duration).toInt()
fun getRemainingTurns(): Int = (assignedOnTurn + getDuration() - gameInfo.turns).coerceAtLeast(0)
fun getInfluence() = questObject.influence
fun getDescription(): String = questObject.description.fillPlaceholders(data1)
fun onClickAction() {
when (questName) {
QuestName.ClearBarbarianCamp.value -> {
when (questNameInstance) {
QuestName.ClearBarbarianCamp -> {
GUI.resetToWorldScreen()
GUI.getMap().setCenterPosition(Vector2(data1.toFloat(), data2.toFloat()), selectUnit = false)
}
QuestName.Route.value -> {
QuestName.Route -> {
GUI.resetToWorldScreen()
GUI.getMap().setCenterPosition(gameInfo.getCivilization(assigner).getCapital()!!.location, selectUnit = false)
}
else -> Unit
}
}
}

View File

@ -57,12 +57,11 @@ object LuxuryResourcePlacementLogic {
// Pick a luxury at random. Weight is reduced if the luxury has been picked before
val regionConditional = StateForConditionals(region = region)
val modifiedWeights = candidateLuxuries.map {
region.luxury = candidateLuxuries.randomWeighted {
val weightingUnique = it.getMatchingUniques(UniqueType.ResourceWeighting, regionConditional).firstOrNull()
val relativeWeight = if (weightingUnique == null) 1f else weightingUnique.params[0].toFloat()
relativeWeight / (1f + amountRegionsWithLuxury[it.name]!!)
}.shuffled()
region.luxury = candidateLuxuries.randomWeighted(modifiedWeights).name
}.name
amountRegionsWithLuxury[region.luxury!!] = amountRegionsWithLuxury[region.luxury]!! + 1
}
@ -150,15 +149,14 @@ object LuxuryResourcePlacementLogic {
}
if (candidateLuxuries.isEmpty()) return@repeat
val weights = candidateLuxuries.map {
val luxury = candidateLuxuries.randomWeighted {
val weightingUnique =
it.getMatchingUniques(UniqueType.LuxuryWeightingForCityStates).firstOrNull()
if (weightingUnique == null)
1f
else
weightingUnique.params[0].toFloat()
}
val luxury = candidateLuxuries.randomWeighted(weights).name
}.name
cityStateLuxuries.add(luxury)
amountRegionsWithLuxury[luxury] = 1
}

View File

@ -50,7 +50,7 @@ object MapRegionResources {
fallbackTiles.add(tile) // Taken but might be a viable fallback tile
} else {
// Add a resource to the tile
val resourceToPlace = possibleResourcesForTile.randomWeighted(possibleResourcesForTile.map { weightings[it] ?: 0f })
val resourceToPlace = possibleResourcesForTile.randomWeighted { weightings[it] ?: 0f }
tile.setTileResource(resourceToPlace, majorDeposit)
tileData.placeImpact(impactType, tile, baseImpact + Random.nextInt(randomImpact + 1))
amountPlaced++
@ -66,7 +66,7 @@ object MapRegionResources {
val bestTile = fallbackTiles.minByOrNull { tileData[it.position]!!.impacts[impactType]!! }!!
fallbackTiles.remove(bestTile)
val possibleResourcesForTile = resourceOptions.filter { it.generatesNaturallyOn(bestTile) }
val resourceToPlace = possibleResourcesForTile.randomWeighted(possibleResourcesForTile.map { weightings[it] ?: 0f })
val resourceToPlace = possibleResourcesForTile.randomWeighted { weightings[it] ?: 0f }
bestTile.setTileResource(resourceToPlace, majorDeposit)
tileData.placeImpact(impactType, bestTile, baseImpact + Random.nextInt(randomImpact + 1))
amountPlaced++

View File

@ -1,7 +1,7 @@
package com.unciv.models.ruleset
import com.unciv.logic.civilization.Civilization
import com.unciv.models.stats.INamed
import com.unciv.logic.civilization.Civilization // for Kdoc
enum class QuestName(val value: String) {
Route("Route"),
@ -22,6 +22,10 @@ enum class QuestName(val value: String) {
DenounceCiv("Denounce Civilization"),
SpreadReligion("Spread Religion"),
None("")
;
companion object {
fun find(value: String) = values().firstOrNull { it.value == value } ?: None
}
}
enum class QuestType {
@ -33,12 +37,14 @@ enum class QuestType {
// 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 */
/** Unique identifier name of the quest, it is also shown.
* Must match a [QuestName.value] for the Quest to have any functionality. */
override var name: String = ""
val questNameInstance by lazy { QuestName.find(name) } // lazy only ensures evaluation happens after deserialization, all will be 'triggered'
/** Description of the quest shown to players */
var description: String = ""

View File

@ -23,6 +23,13 @@ fun <T> List<T>.randomWeighted(weights: List<Float>, random: Random = Random): T
return this.last()
}
/** Get one random element of a given List.
*
* The probability for each element is proportional to the result of [getWeight] (evaluated only once).
*/
fun <T> List<T>.randomWeighted(random: Random = Random, getWeight: (T) -> Float): T =
randomWeighted(map(getWeight), random)
/** Gets a clone of an [ArrayList] with an additional item
*
* Solves concurrent modification problems - everyone who had a reference to the previous arrayList can keep using it because it hasn't changed

View File

@ -81,7 +81,7 @@ class CityStateDiplomacyTable(private val diplomacyScreen: DiplomacyScreen) {
val diplomaticMarriageButton = getDiplomaticMarriageButton(otherCiv)
if (diplomaticMarriageButton != null) diplomacyTable.add(diplomaticMarriageButton).row()
for (assignedQuest in otherCiv.questManager.assignedQuests.filter { it.assignee == viewingCiv.civName }) {
for (assignedQuest in otherCiv.questManager.getAssignedQuestsFor(viewingCiv.civName)) {
diplomacyTable.addSeparator()
diplomacyTable.add(getQuestTable(assignedQuest)).row()
}
@ -464,8 +464,8 @@ class CityStateDiplomacyTable(private val diplomacyScreen: DiplomacyScreen) {
if (quest.duration > 0)
questTable.add("[${remainingTurns}] turns remaining".toLabel()).row()
if (quest.isGlobal()) {
val leaderString = viewingCiv.gameInfo.getCivilization(assignedQuest.assigner).questManager.getLeaderStringForQuest(assignedQuest.questName)
if (leaderString != "")
val leaderString = viewingCiv.gameInfo.getCivilization(assignedQuest.assigner).questManager.getScoreStringForGlobalQuest(assignedQuest)
if (leaderString.isNotEmpty())
questTable.add(leaderString.toLabel()).row()
}

View File

@ -162,8 +162,7 @@ class WonderInfo {
private fun knownFromQuest(viewingPlayer: Civilization, name: String): Boolean {
// No, *your* civInfo's QuestManager has no idea about your quests
for (civ in gameInfo.civilizations) {
for (quest in civ.questManager.assignedQuests) {
if (quest.assignee != viewingPlayer.civName) continue
for (quest in civ.questManager.getAssignedQuestsFor(viewingPlayer.civName)) {
if (quest.questName == QuestName.FindNaturalWonder.value && quest.data1 == name)
return true
}