diff --git a/android/assets/jsons/translations/French.properties b/android/assets/jsons/translations/French.properties index 00b8f68db7..8142e48b0d 100644 --- a/android/assets/jsons/translations/French.properties +++ b/android/assets/jsons/translations/French.properties @@ -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. diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index c7517786fd..6027085477 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -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. = diff --git a/core/src/com/unciv/logic/civilization/managers/QuestManager.kt b/core/src/com/unciv/logic/civilization/managers/QuestManager.kt index 75aac4985c..a5d9d394fd 100644 --- a/core/src/com/unciv/logic/civilization/managers/QuestManager.kt +++ b/core/src/com/unciv/logic/civilization/managers/QuestManager.kt @@ -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 = ArrayList() + private var assignedQuests: ArrayList = 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> = 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() - 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,204 +309,277 @@ class QuestManager : IsPartOfGameInfoSerialization { private fun assignNewQuest(quest: Quest, assignees: Iterable) { - 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 = listOf(DiplomacyAction(civInfo.civName)) + var notificationActions: List = 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 = 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() + val losers = mutableListOf() + 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]. * Since [QuestName.ClearBarbarianCamp] is a global quest, it could have been assigned to * 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) { - // Revoke most quest types from the bully - val revokedQuests = assignedQuests.asSequence() - .filter { it.assignee == bully.civName && (it.isIndividual() || it.questName == QuestName.Invest.value) } - 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 (civ != cityState) return + + // Revoke most quest types from the bully + val revokedQuests = getAssignedQuestsFor(bully.civName) + .filter { it.isIndividual() || it.questNameInstance == QuestName.Invest } + .toList() + assignedQuests.removeAll(revokedQuests) + 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,9 +723,8 @@ class QuestManager : IsPartOfGameInfoSerialization { * Gets notified when given gold by [donorCiv]. */ fun receivedGoldGift(donorCiv: Civilization) { - val matchingQuests = assignedQuests.asSequence() - .filter { it.questName == QuestName.GiveGold.value } - .filter { it.assignee == donorCiv.civName} + val matchingQuests = getAssignedQuestsOfName(QuestName.GiveGold) + .filter { it.assignee == donorCiv.civName } for (quest in matchingQuests) giveReward(quest) @@ -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 = "", - val assigner: String = "", - val assignee: String = "", - val assignedOnTurn: Int = 0, - val data1: String = "", - val data2: String = "") : IsPartOfGameInfoSerialization { +class AssignedQuest( + val questName: String = "", + val assigner: String = "", + val assignee: String = "", + val assignedOnTurn: Int = 0, + val data1: String = "", + 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 } } } diff --git a/core/src/com/unciv/logic/map/mapgenerator/mapregions/LuxuryResourcePlacementLogic.kt b/core/src/com/unciv/logic/map/mapgenerator/mapregions/LuxuryResourcePlacementLogic.kt index 15e2dfbccc..399030f531 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/mapregions/LuxuryResourcePlacementLogic.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/mapregions/LuxuryResourcePlacementLogic.kt @@ -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 } diff --git a/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegionResources.kt b/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegionResources.kt index 620cd64d77..9d684dfc48 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegionResources.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegionResources.kt @@ -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++ diff --git a/core/src/com/unciv/models/ruleset/Quest.kt b/core/src/com/unciv/models/ruleset/Quest.kt index b2bf4c1e30..ab37107a6b 100644 --- a/core/src/com/unciv/models/ruleset/Quest.kt +++ b/core/src/com/unciv/models/ruleset/Quest.kt @@ -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 = "" diff --git a/core/src/com/unciv/ui/components/extensions/CollectionExtensions.kt b/core/src/com/unciv/ui/components/extensions/CollectionExtensions.kt index c0ac300454..6bef1b4f63 100644 --- a/core/src/com/unciv/ui/components/extensions/CollectionExtensions.kt +++ b/core/src/com/unciv/ui/components/extensions/CollectionExtensions.kt @@ -23,6 +23,13 @@ fun List.randomWeighted(weights: List, 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 List.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 diff --git a/core/src/com/unciv/ui/screens/diplomacyscreen/CityStateDiplomacyTable.kt b/core/src/com/unciv/ui/screens/diplomacyscreen/CityStateDiplomacyTable.kt index 728f2162d9..277a7953a7 100644 --- a/core/src/com/unciv/ui/screens/diplomacyscreen/CityStateDiplomacyTable.kt +++ b/core/src/com/unciv/ui/screens/diplomacyscreen/CityStateDiplomacyTable.kt @@ -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() } diff --git a/core/src/com/unciv/ui/screens/overviewscreen/WonderOverviewTab.kt b/core/src/com/unciv/ui/screens/overviewscreen/WonderOverviewTab.kt index 7da7c256db..75fd1be801 100644 --- a/core/src/com/unciv/ui/screens/overviewscreen/WonderOverviewTab.kt +++ b/core/src/com/unciv/ui/screens/overviewscreen/WonderOverviewTab.kt @@ -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 }