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) [questName] (+[influenceAmount] influence) = [questName] (+[influenceAmount] influence)
[remainingTurns] turns remaining = [remainingTurns] tours restants [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(s): [leaders] = Actuellement en tête: [leaders]
Current leader is [civInfo] with [amount] Technologies discovered. = [civInfo] est actuellement en tête avec [amount] Technologies découvertes. 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 Demands = Demandes
Please don't settle new cities near us. = Veuillez ne pas fonder de villes près de nous. 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) = [questName] (+[influenceAmount] influence) =
[remainingTurns] turns remaining = [remainingTurns] turns remaining =
Current leader is [civInfo] with [amount] [stat] generated. = Current leader(s): [leaders] =
Current leader is [civInfo] with [amount] Technologies discovered. = 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 = Demands =
Please don't settle new cities near us. = 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.ruleset.unit.BaseUnit
import com.unciv.models.translations.fillPlaceholders import com.unciv.models.translations.fillPlaceholders
import com.unciv.models.translations.getPlaceholderParameters 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.randomWeighted
import com.unciv.ui.components.extensions.toPercent import com.unciv.ui.components.extensions.toPercent
import kotlin.math.max
import kotlin.random.Random import kotlin.random.Random
class QuestManager : IsPartOfGameInfoSerialization { class QuestManager : IsPartOfGameInfoSerialization {
@ -58,10 +58,13 @@ class QuestManager : IsPartOfGameInfoSerialization {
/** Civilization object holding and dispatching quests */ /** Civilization object holding and dispatching quests */
@Transient @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*/ /** 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 */ /** Number of turns left before starting new global quest */
private var globalQuestCountdown: Int = UNSET private var globalQuestCountdown: Int = UNSET
@ -76,15 +79,24 @@ class QuestManager : IsPartOfGameInfoSerialization {
/** For this attacker, number of units killed by each civ */ /** For this attacker, number of units killed by each civ */
private var unitsKilledFromCiv: HashMap<String, HashMap<String, Int>> = HashMap() private var unitsKilledFromCiv: HashMap<String, HashMap<String, Int>> = HashMap()
/** Returns true if [civInfo] have active quests for [challenger] */ /** Returns true if [civ] have active quests for [challenger] */
fun haveQuestsFor(challenger: Civilization): Boolean = assignedQuests.any { it.assignee == challenger.civName } fun haveQuestsFor(challenger: Civilization): Boolean = getAssignedQuestsFor(challenger.civName).any()
/** Returns true if [civInfo] has asked anyone to conquer [target] */ /** Access all assigned Quests for [civName] */
fun wantsDead(target: String): Boolean = assignedQuests.any { it.questName == QuestName.ConquerCityState.value && it.data1 == target } 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 { 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 1f
return investmentQuest.data1.toPercent() return investmentQuest.data1.toPercent()
} }
@ -96,31 +108,30 @@ class QuestManager : IsPartOfGameInfoSerialization {
toReturn.assignedQuests.addAll(assignedQuests) toReturn.assignedQuests.addAll(assignedQuests)
toReturn.unitsToKillForCiv.putAll(unitsToKillForCiv) toReturn.unitsToKillForCiv.putAll(unitsToKillForCiv)
for ((attacker, unitsKilled) in unitsKilledFromCiv) { for ((attacker, unitsKilled) in unitsKilledFromCiv) {
toReturn.unitsKilledFromCiv[attacker] = HashMap() toReturn.unitsKilledFromCiv[attacker] = HashMap(unitsKilled)
toReturn.unitsKilledFromCiv[attacker]!!.putAll(unitsKilled)
} }
return toReturn return toReturn
} }
fun setTransients(civInfo: Civilization) { fun setTransients(civ: Civilization) {
this.civInfo = civInfo this.civ = civ
for (quest in assignedQuests) for (quest in assignedQuests)
quest.gameInfo = civInfo.gameInfo quest.setTransients(civ.gameInfo)
} }
fun endTurn() { fun endTurn() {
if (civInfo.isDefeated()) { if (civ.isDefeated()) {
assignedQuests.clear() assignedQuests.clear()
individualQuestCountdown.clear() individualQuestCountdown.clear()
globalQuestCountdown = UNSET globalQuestCountdown = UNSET
return 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() seedGlobalQuestCountdown()
seedIndividualQuestsCountdown() seedIndividualQuestsCountdowns()
decrementQuestCountdowns() decrementQuestCountdowns()
@ -144,26 +155,26 @@ class QuestManager : IsPartOfGameInfoSerialization {
} }
private fun seedGlobalQuestCountdown() { private fun seedGlobalQuestCountdown() {
if (civInfo.gameInfo.turns < GLOBAL_QUEST_FIRST_POSSIBLE_TURN) if (civ.gameInfo.turns < GLOBAL_QUEST_FIRST_POSSIBLE_TURN)
return return
if (globalQuestCountdown != UNSET) if (globalQuestCountdown != UNSET)
return return
val countdown = 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) Random.nextInt(GLOBAL_QUEST_FIRST_POSSIBLE_TURN_RAND)
else else
GLOBAL_QUEST_MIN_TURNS_BETWEEN + Random.nextInt(GLOBAL_QUEST_RAND_TURNS_BETWEEN) 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() { private fun seedIndividualQuestsCountdowns() {
if (civInfo.gameInfo.turns < INDIVIDUAL_QUEST_FIRST_POSSIBLE_TURN) if (civ.gameInfo.turns < INDIVIDUAL_QUEST_FIRST_POSSIBLE_TURN)
return return
val majorCivs = civInfo.gameInfo.getAliveMajorCivs() val majorCivs = civ.gameInfo.getAliveMajorCivs()
for (majorCiv in majorCivs) for (majorCiv in majorCivs)
if (!individualQuestCountdown.containsKey(majorCiv.civName) || individualQuestCountdown[majorCiv.civName] == UNSET) if (!individualQuestCountdown.containsKey(majorCiv.civName) || individualQuestCountdown[majorCiv.civName] == UNSET)
seedIndividualQuestsCountdown(majorCiv) seedIndividualQuestsCountdown(majorCiv)
@ -171,36 +182,34 @@ class QuestManager : IsPartOfGameInfoSerialization {
private fun seedIndividualQuestsCountdown(challenger: Civilization) { private fun seedIndividualQuestsCountdown(challenger: Civilization) {
val countdown: Int = 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) Random.nextInt(INDIVIDUAL_QUEST_FIRST_POSSIBLE_TURN_RAND)
else else
INDIVIDUAL_QUEST_MIN_TURNS_BETWEEN + Random.nextInt( INDIVIDUAL_QUEST_MIN_TURNS_BETWEEN + Random.nextInt(
INDIVIDUAL_QUEST_RAND_TURNS_BETWEEN 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() { private fun tryStartNewGlobalQuest() {
if (globalQuestCountdown != 0) if (globalQuestCountdown != 0)
return return
if (assignedQuests.count { it.isGlobal() } >= GLOBAL_QUEST_MAX_ACTIVE) if (assignedQuests.count { it.isGlobal() } >= GLOBAL_QUEST_MAX_ACTIVE)
return return
val globalQuests = civInfo.gameInfo.ruleset.quests.values.filter { it.isGlobal() } val majorCivs = civ.getKnownCivs().filter { it.isMajorCiv() && !it.isAtWarWith(civ) } // A Sequence - fine because the count below can be different for each Quest
val majorCivs = civInfo.getKnownCivs().filter { it.isMajorCiv() && !it.isAtWarWith(civInfo) } fun Quest.isAssignable() = majorCivs.count { civ -> isQuestValid(this, civ) } >= minimumCivs
val assignableQuests = getQuests {
val assignableQuests = ArrayList<Quest>() it.isGlobal() && it.isAssignable()
for (quest in globalQuests) {
val numberValidMajorCivs = majorCivs.count { civ -> isQuestValid(quest, civ) }
if (numberValidMajorCivs >= quest.minimumCivs)
assignableQuests.add(quest)
} }
val weights = assignableQuests.map { getQuestWeight(it.name) }
if (assignableQuests.isNotEmpty()) { if (assignableQuests.isNotEmpty()) {
val quest = assignableQuests.randomWeighted(weights) val quest = assignableQuests.randomWeighted { getQuestWeight(it.name) }
val assignees = civInfo.gameInfo.getAliveMajorCivs().filter { !it.isAtWarWith(civInfo) && isQuestValid(quest, it) } val assignees = civ.gameInfo.getAliveMajorCivs().filter { !it.isAtWarWith(civ) && isQuestValid(quest, it) }
assignNewQuest(quest, assignees) assignNewQuest(quest, assignees)
globalQuestCountdown = UNSET globalQuestCountdown = UNSET
@ -209,19 +218,18 @@ class QuestManager : IsPartOfGameInfoSerialization {
private fun tryStartNewIndividualQuests() { private fun tryStartNewIndividualQuests() {
for ((challengerName, countdown) in individualQuestCountdown) { for ((challengerName, countdown) in individualQuestCountdown) {
val challenger = civInfo.gameInfo.getCivilization(challengerName) val challenger = civ.gameInfo.getCivilization(challengerName)
if (countdown != 0) if (countdown != 0)
continue 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 continue
val assignableQuests = civInfo.gameInfo.ruleset.quests.values.filter { it.isIndividual() && isQuestValid(it, challenger) } val assignableQuests = getQuests { it.isIndividual() && isQuestValid(it, challenger) }
val weights = assignableQuests.map { getQuestWeight(it.name) }
if (assignableQuests.isNotEmpty()) { if (assignableQuests.isNotEmpty()) {
val quest = assignableQuests.randomWeighted(weights) val quest = assignableQuests.randomWeighted { getQuestWeight(it.name) }
val assignees = arrayListOf(challenger) val assignees = arrayListOf(challenger)
assignNewQuest(quest, assignees) assignNewQuest(quest, assignees)
@ -230,46 +238,42 @@ class QuestManager : IsPartOfGameInfoSerialization {
} }
private fun tryBarbarianInvasion() { private fun tryBarbarianInvasion() {
if ((civInfo.getTurnsTillCallForBarbHelp() == null || civInfo.getTurnsTillCallForBarbHelp() == 0) if ((civ.getTurnsTillCallForBarbHelp() == null || civ.getTurnsTillCallForBarbHelp() == 0)
&& civInfo.cityStateFunctions.getNumThreateningBarbarians() >= 2) { && civ.cityStateFunctions.getNumThreateningBarbarians() >= 2) {
for (otherCiv in civInfo.getKnownCivs().filter { for (otherCiv in civ.getKnownCivs().filter {
it.isMajorCiv() it.isMajorCiv()
&& it.isAlive() && it.isAlive()
&& !it.isAtWarWith(civInfo) && !it.isAtWarWith(civ)
&& it.getProximity(civInfo) <= Proximity.Far && it.getProximity(civ) <= Proximity.Far
}) { }) {
otherCiv.addNotification("[${civInfo.civName}] is being invaded by Barbarians! Destroy Barbarians near their territory to earn Influence.", otherCiv.addNotification(
civInfo.getCapital()!!.location, "[${civ.civName}] is being invaded by Barbarians! Destroy Barbarians near their territory to earn Influence.",
NotificationCategory.Diplomacy, civInfo.civName, civ.getCapital()!!.location,
NotificationCategory.Diplomacy, civ.civName,
NotificationIcon.War NotificationIcon.War
) )
} }
civInfo.addFlag(CivFlags.TurnsTillCallForBarbHelp.name, 30) civ.addFlag(CivFlags.TurnsTillCallForBarbHelp.name, 30)
} }
} }
private fun handleGlobalQuests() { private fun handleGlobalQuests() {
// Remove any participants that are no longer valid because of being dead or at war with the CS // Remove any participants that are no longer valid because of being dead or at war with the CS
assignedQuests.removeAll { it.isGlobal() && assignedQuests.removeAll { it.isGlobal() &&
!canAssignAQuestTo(civInfo.gameInfo.getCivilization(it.assignee)) } !canAssignAQuestTo(civ.gameInfo.getCivilization(it.assignee)) }
val globalQuestsExpired = assignedQuests.filter { it.isGlobal() && it.isExpired() }.map { it.questName }.distinct() val globalQuestsExpired = assignedQuests.filter { it.isGlobal() && it.isExpired() }.map { it.questNameInstance }.distinct()
for (globalQuestName in globalQuestsExpired) for (globalQuestName in globalQuestsExpired)
handleGlobalQuest(globalQuestName) handleGlobalQuest(globalQuestName)
} }
private fun handleGlobalQuest(questName: String) { private fun handleGlobalQuest(questName: QuestName) {
val quests = assignedQuests.filter { it.questName == questName } val winnersAndLosers = WinnersAndLosers(questName)
if (quests.isEmpty()) winnersAndLosers.winners.forEach { giveReward(it) }
return winnersAndLosers.losers.forEach { notifyExpired(it, winnersAndLosers.winners) }
val topScore = quests.maxOf { getScoreForQuest(it) } assignedQuests.removeAll(winnersAndLosers.winners)
val winners = quests.filter { getScoreForQuest(it) == topScore } assignedQuests.removeAll(winnersAndLosers.losers)
winners.forEach { giveReward(it) }
for (loser in quests.filterNot { it in winners })
notifyExpired(loser, winners)
assignedQuests.removeAll(quests)
} }
private fun handleIndividualQuests() { private fun handleIndividualQuests() {
@ -279,7 +283,7 @@ class QuestManager : IsPartOfGameInfoSerialization {
/** If quest is complete, it gives the influence reward to the player. /** 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) */ * Returns true if the quest can be removed (is either complete, obsolete or expired) */
private fun handleIndividualQuest(assignedQuest: AssignedQuest): Boolean { 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 // One of the civs is defeated, or they started a war: remove quest
if (!canAssignAQuestTo(assignee)) if (!canAssignAQuestTo(assignee))
@ -305,195 +309,269 @@ class QuestManager : IsPartOfGameInfoSerialization {
private fun assignNewQuest(quest: Quest, assignees: Iterable<Civilization>) { private fun assignNewQuest(quest: Quest, assignees: Iterable<Civilization>) {
val turn = civInfo.gameInfo.turns val turn = civ.gameInfo.turns
for (assignee in assignees) { for (assignee in assignees) {
val playerReligion = civInfo.gameInfo.religions.values.firstOrNull { it.foundingCivName == assignee.civName && it.isMajorReligion() }
var data1 = "" var data1 = ""
var data2 = "" var data2 = ""
var notificationActions: List<NotificationAction> = listOf(DiplomacyAction(civInfo.civName)) var notificationActions: List<NotificationAction> = listOf(DiplomacyAction(civ.civName))
when (quest.name) { when (quest.questNameInstance) {
QuestName.ClearBarbarianCamp.value -> { QuestName.ClearBarbarianCamp -> {
val camp = getBarbarianEncampmentForQuest()!! val camp = getBarbarianEncampmentForQuest()!!
data1 = camp.position.x.toInt().toString() data1 = camp.position.x.toInt().toString()
data2 = camp.position.y.toInt().toString() data2 = camp.position.y.toInt().toString()
notificationActions = listOf(LocationAction(camp.position), notificationActions.first()) notificationActions = listOf(LocationAction(camp.position), notificationActions.first())
} }
QuestName.ConnectResource.value -> data1 = getResourceForQuest(assignee)!!.name QuestName.ConnectResource -> data1 = getResourceForQuest(assignee)!!.name
QuestName.ConstructWonder.value -> data1 = getWonderToBuildForQuest(assignee)!!.name QuestName.ConstructWonder -> data1 = getWonderToBuildForQuest(assignee)!!.name
QuestName.GreatPerson.value -> data1 = getGreatPersonForQuest(assignee)!!.name QuestName.GreatPerson -> data1 = getGreatPersonForQuest(assignee)!!.name
QuestName.FindPlayer.value -> data1 = getCivilizationToFindForQuest(assignee)!!.civName QuestName.FindPlayer -> data1 = getCivilizationToFindForQuest(assignee)!!.civName
QuestName.FindNaturalWonder.value -> data1 = getNaturalWonderToFindForQuest(assignee)!! QuestName.FindNaturalWonder -> data1 = getNaturalWonderToFindForQuest(assignee)!!
QuestName.ConquerCityState.value -> data1 = getCityStateTarget(assignee)!!.civName QuestName.ConquerCityState -> data1 = getCityStateTarget(assignee)!!.civName
QuestName.BullyCityState.value -> data1 = getCityStateTarget(assignee)!!.civName QuestName.BullyCityState -> data1 = getCityStateTarget(assignee)!!.civName
QuestName.PledgeToProtect.value -> data1 = getMostRecentBully()!! QuestName.PledgeToProtect -> data1 = getMostRecentBully()!!
QuestName.GiveGold.value -> data1 = getMostRecentBully()!! QuestName.GiveGold -> data1 = getMostRecentBully()!!
QuestName.DenounceCiv.value -> data1 = getMostRecentBully()!! QuestName.DenounceCiv -> data1 = getMostRecentBully()!!
QuestName.SpreadReligion.value -> { QuestName.SpreadReligion -> {
data1 = playerReligion!!.getReligionDisplayName() // For display 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 data2 = playerReligion.name // To check completion
} }
QuestName.ContestCulture.value -> data1 = assignee.totalCultureForContests.toString() QuestName.ContestCulture -> data1 = assignee.totalCultureForContests.toString()
QuestName.ContestFaith.value -> data1 = assignee.totalFaithForContests.toString() QuestName.ContestFaith -> data1 = assignee.totalFaithForContests.toString()
QuestName.ContestTech.value -> data1 = assignee.tech.getNumberOfTechsResearched().toString() QuestName.ContestTech -> data1 = assignee.tech.getNumberOfTechsResearched().toString()
QuestName.Invest.value -> data1 = quest.description.getPlaceholderParameters().first() QuestName.Invest -> data1 = quest.description.getPlaceholderParameters().first()
else -> Unit
} }
val newQuest = AssignedQuest( val newQuest = AssignedQuest(
questName = quest.name, questName = quest.name,
assigner = civInfo.civName, assigner = civ.civName,
assignee = assignee.civName, assignee = assignee.civName,
assignedOnTurn = turn, assignedOnTurn = turn,
data1 = data1, data1 = data1,
data2 = data2 data2 = data2
) )
newQuest.gameInfo = civInfo.gameInfo newQuest.setTransients(civ.gameInfo, quest)
assignedQuests.add(newQuest) assignedQuests.add(newQuest)
assignee.addNotification("[${civInfo.civName}] assigned you a new quest: [${quest.name}].",
notificationActions,
NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest")
if (quest.isIndividual()) if (quest.isIndividual())
individualQuestCountdown[assignee.civName] = UNSET 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 { private fun canAssignAQuestTo(challenger: Civilization): Boolean {
return !challenger.isDefeated() && challenger.isMajorCiv() && 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] */ /** Returns true if the [quest] can be assigned to [challenger] */
private fun isQuestValid(quest: Quest, challenger: Civilization): Boolean { private fun isQuestValid(quest: Quest, challenger: Civilization): Boolean {
if (!canAssignAQuestTo(challenger)) if (!canAssignAQuestTo(challenger))
return false return false
if (assignedQuests.any { it.assignee == challenger.civName && it.questName == quest.name }) if (getAssignedQuestsOfName(quest.questNameInstance).any { it.assignee == challenger.civName })
return false return false
if (quest.isIndividual() && civInfo.getDiplomacyManager(challenger).hasFlag(DiplomacyFlags.Bullied)) if (quest.isIndividual() && civ.getDiplomacyManager(challenger).hasFlag(DiplomacyFlags.Bullied))
return false return false
val mostRecentBully = getMostRecentBully() return when (quest.questNameInstance) {
val playerReligion = civInfo.gameInfo.religions.values.firstOrNull { it.foundingCivName == challenger.civName && it.isMajorReligion() }?.name QuestName.ClearBarbarianCamp -> getBarbarianEncampmentForQuest() != null
QuestName.Route -> isRouteQuestValid(challenger)
return when (quest.name) { QuestName.ConnectResource -> getResourceForQuest(challenger) != null
QuestName.ClearBarbarianCamp.value -> getBarbarianEncampmentForQuest() != null QuestName.ConstructWonder -> getWonderToBuildForQuest(challenger) != null
QuestName.Route.value -> !challenger.cities.none() QuestName.GreatPerson -> getGreatPersonForQuest(challenger) != null
&& !challenger.isCapitalConnectedToCity(civInfo.getCapital()!!) QuestName.FindPlayer -> getCivilizationToFindForQuest(challenger) != null
// Need to have a city within 7 tiles on the same continent QuestName.FindNaturalWonder -> getNaturalWonderToFindForQuest(challenger) != null
&& challenger.cities.any { it.getCenterTile().aerialDistanceTo(civInfo.getCapital()!!.getCenterTile()) <= 7 QuestName.PledgeToProtect -> getMostRecentBully() != null && challenger !in civ.cityStateFunctions.getProtectorCivs()
&& it.getCenterTile().getContinent() == civInfo.getCapital()!!.getCenterTile().getContinent() } QuestName.GiveGold -> getMostRecentBully() != null
QuestName.ConnectResource.value -> getResourceForQuest(challenger) != null QuestName.DenounceCiv -> isDenounceCivQuestValid(challenger, getMostRecentBully())
QuestName.ConstructWonder.value -> getWonderToBuildForQuest(challenger) != null QuestName.SpreadReligion -> {
QuestName.GreatPerson.value -> getGreatPersonForQuest(challenger) != null val playerReligion = civ.gameInfo.religions.values.firstOrNull { it.foundingCivName == challenger.civName && it.isMajorReligion() }?.name
QuestName.FindPlayer.value -> getCivilizationToFindForQuest(challenger) != null playerReligion != null && civ.getCapital()!!.religion.getMajorityReligion()?.name != playerReligion
QuestName.FindNaturalWonder.value -> getNaturalWonderToFindForQuest(challenger) != null }
QuestName.PledgeToProtect.value -> mostRecentBully != null && challenger !in civInfo.cityStateFunctions.getProtectorCivs() QuestName.ConquerCityState -> getCityStateTarget(challenger) != null && civ.cityStatePersonality != CityStatePersonality.Friendly
QuestName.GiveGold.value -> mostRecentBully != null QuestName.BullyCityState -> getCityStateTarget(challenger) != null
QuestName.DenounceCiv.value -> mostRecentBully != null && challenger.knows(mostRecentBully) QuestName.ContestFaith -> civ.gameInfo.isReligionEnabled()
&& !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()
else -> true 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 */ /** Returns true if the [assignedQuest] is successfully completed */
private fun isComplete(assignedQuest: AssignedQuest): Boolean { private fun isComplete(assignedQuest: AssignedQuest): Boolean {
val assignee = civInfo.gameInfo.getCivilization(assignedQuest.assignee) val assignee = civ.gameInfo.getCivilization(assignedQuest.assignee)
return when (assignedQuest.questName) { return when (assignedQuest.questNameInstance) {
QuestName.Route.value -> assignee.isCapitalConnectedToCity(civInfo.getCapital()!!) QuestName.Route -> assignee.isCapitalConnectedToCity(civ.getCapital()!!)
QuestName.ConnectResource.value -> assignee.detailedCivResources.map { it.resource }.contains(civInfo.gameInfo.ruleset.tileResources[assignedQuest.data1]) QuestName.ConnectResource -> assignee.detailedCivResources.map { it.resource }.contains(ruleset.tileResources[assignedQuest.data1])
QuestName.ConstructWonder.value -> assignee.cities.any { it.cityConstructions.isBuilt(assignedQuest.data1) } QuestName.ConstructWonder -> 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.GreatPerson -> assignee.units.getCivGreatPeople().any { it.baseUnit.getReplacedUnit(ruleset).name == assignedQuest.data1 }
QuestName.FindPlayer.value -> assignee.hasMetCivTerritory(civInfo.gameInfo.getCivilization(assignedQuest.data1)) QuestName.FindPlayer -> assignee.hasMetCivTerritory(civ.gameInfo.getCivilization(assignedQuest.data1))
QuestName.FindNaturalWonder.value -> assignee.naturalWonders.contains(assignedQuest.data1) QuestName.FindNaturalWonder -> assignee.naturalWonders.contains(assignedQuest.data1)
QuestName.PledgeToProtect.value -> assignee in civInfo.cityStateFunctions.getProtectorCivs() QuestName.PledgeToProtect -> assignee in civ.cityStateFunctions.getProtectorCivs()
QuestName.DenounceCiv.value -> assignee.getDiplomacyManager(assignedQuest.data1).hasFlag(DiplomacyFlags.Denunciation) QuestName.DenounceCiv -> assignee.getDiplomacyManager(assignedQuest.data1).hasFlag(DiplomacyFlags.Denunciation)
QuestName.SpreadReligion.value -> civInfo.getCapital()!!.religion.getMajorityReligion() == civInfo.gameInfo.religions[assignedQuest.data2] QuestName.SpreadReligion -> civ.getCapital()!!.religion.getMajorityReligion() == civ.gameInfo.religions[assignedQuest.data2]
else -> false else -> false
} }
} }
/** Returns true if the [assignedQuest] request cannot be fulfilled anymore */ /** Returns true if the [assignedQuest] request cannot be fulfilled anymore */
private fun isObsolete(assignedQuest: AssignedQuest): Boolean { private fun isObsolete(assignedQuest: AssignedQuest): Boolean {
val assignee = civInfo.gameInfo.getCivilization(assignedQuest.assignee) val assignee = civ.gameInfo.getCivilization(assignedQuest.assignee)
return when (assignedQuest.questName) { return when (assignedQuest.questNameInstance) {
QuestName.ClearBarbarianCamp.value -> civInfo.gameInfo.tileMap[assignedQuest.data1.toInt(), assignedQuest.data2.toInt()].improvement != Constants.barbarianEncampment QuestName.ClearBarbarianCamp -> civ.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.ConstructWonder -> civ.gameInfo.getCities().any { it.civ != assignee && it.cityConstructions.isBuilt(assignedQuest.data1) }
QuestName.FindPlayer.value -> civInfo.gameInfo.getCivilization(assignedQuest.data1).isDefeated() QuestName.FindPlayer -> civ.gameInfo.getCivilization(assignedQuest.data1).isDefeated()
QuestName.ConquerCityState.value -> civInfo.gameInfo.getCivilization(assignedQuest.data1).isDefeated() QuestName.ConquerCityState -> civ.gameInfo.getCivilization(assignedQuest.data1).isDefeated()
QuestName.BullyCityState.value -> civInfo.gameInfo.getCivilization(assignedQuest.data1).isDefeated() QuestName.BullyCityState -> civ.gameInfo.getCivilization(assignedQuest.data1).isDefeated()
QuestName.DenounceCiv.value -> civInfo.gameInfo.getCivilization(assignedQuest.data1).isDefeated() QuestName.DenounceCiv -> civ.gameInfo.getCivilization(assignedQuest.data1).isDefeated()
else -> false 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) { private fun giveReward(assignedQuest: AssignedQuest) {
val rewardInfluence = civInfo.gameInfo.ruleset.quests[assignedQuest.questName]!!.influence val rewardInfluence = assignedQuest.getInfluence()
val assignee = civInfo.gameInfo.getCivilization(assignedQuest.assignee) val assignee = civ.gameInfo.getCivilization(assignedQuest.assignee)
civInfo.getDiplomacyManager(assignedQuest.assignee).addInfluence(rewardInfluence) civ.getDiplomacyManager(assignedQuest.assignee).addInfluence(rewardInfluence)
if (rewardInfluence > 0) if (rewardInfluence > 0)
assignee.addNotification( assignee.addNotification(
"[${civInfo.civName}] rewarded you with [${rewardInfluence.toInt()}] influence for completing the [${assignedQuest.questName}] quest.", "[${civ.civName}] rewarded you with [${rewardInfluence.toInt()}] influence for completing the [${assignedQuest.questName}] quest.",
civInfo.getCapital()!!.location, NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest" civ.getCapital()!!.location, NotificationCategory.Diplomacy, civ.civName, "OtherIcons/Quest"
) )
// We may have received bonuses from city-state friend-ness or ally-ness // 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() city.cityStats.update()
} }
/** Notifies the assignee of [assignedQuest] that the quest is now obsolete or expired. /** Notifies the assignee of [assignedQuest] that the quest is now obsolete or expired.
* Optionally displays the [winners] of global quests. */ * Optionally displays the [winners] of global quests. */
private fun notifyExpired(assignedQuest: AssignedQuest, winners: List<AssignedQuest> = emptyList()) { 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()) { if (winners.isEmpty()) {
assignee.addNotification( assignee.addNotification(
"[${civInfo.civName}] no longer needs your help with the [${assignedQuest.questName}] quest.", "[${civ.civName}] no longer needs your help with the [${assignedQuest.questName}] quest.",
civInfo.getCapital()!!.location, civ.getCapital()!!.location,
NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest") NotificationCategory.Diplomacy, civ.civName, "OtherIcons/Quest")
} else { } else {
assignee.addNotification( assignee.addNotification(
"The [${assignedQuest.questName}] quest for [${civInfo.civName}] has ended. It was won by [${winners.joinToString { "{${it.assignee}}" }}].", "The [${assignedQuest.questName}] quest for [${civ.civName}] has ended. It was won by [${winners.joinToString { "{${it.assignee}}" }}].",
civInfo.getCapital()!!.location, civ.getCapital()!!.location,
NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest") NotificationCategory.Diplomacy, civ.civName, "OtherIcons/Quest")
} }
} }
/** Returns the score for the [assignedQuest] */ /** Returns the score for the [assignedQuest] */
private fun getScoreForQuest(assignedQuest: AssignedQuest): Int { private fun getScoreForQuest(assignedQuest: AssignedQuest): Int {
val assignee = civInfo.gameInfo.getCivilization(assignedQuest.assignee) val assignee = civ.gameInfo.getCivilization(assignedQuest.assignee)
return when (assignedQuest.questName) {
QuestName.ContestCulture.value -> assignee.totalCultureForContests - assignedQuest.data1.toInt() return when (assignedQuest.questNameInstance) {
QuestName.ContestFaith.value -> assignee.totalFaithForContests - assignedQuest.data1.toInt() //quest total = civ total - the value at the time the quest started (which was stored in assignedQuest.data1)
QuestName.ContestTech.value -> assignee.tech.getNumberOfTechsResearched() - assignedQuest.data1.toInt() QuestName.ContestCulture -> assignee.totalCultureForContests - assignedQuest.data1.toInt()
QuestName.ContestFaith -> assignee.totalFaithForContests - assignedQuest.data1.toInt()
QuestName.ContestTech -> assignee.tech.getNumberOfTechsResearched() - assignedQuest.data1.toInt()
else -> 0 else -> 0
} }
} }
/** Returns a string with the leading civ and their score for [questName] */ /** Evaluate a contest-type quest:
fun getLeaderStringForQuest(questName: String): String { *
val leadingQuest = assignedQuests.filter { it.questName == questName }.maxByOrNull { getScoreForQuest(it) } * - Determines [winner(s)][winners] (as AssignedQuest instances, which name their assignee): Those whose score is the [maximum score][maxScore], possibly tied.
?: return "" * 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) { init {
QuestName.ContestCulture.value -> "Current leader is [${leadingQuest.assignee}] with [${getScoreForQuest(leadingQuest)}] [Culture] generated." require(ruleset.quests[questName.value]!!.isGlobal())
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." for (quest in getAssignedQuestsOfName(questName)) {
else -> "" 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]. * 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. * multiple civilizations, so after this notification all matching quests are removed.
*/ */
fun barbarianCampCleared(civInfo: Civilization, location: Vector2) { fun barbarianCampCleared(civInfo: Civilization, location: Vector2) {
val matchingQuests = assignedQuests.asSequence() val matchingQuests = getAssignedQuestsOfName(QuestName.ClearBarbarianCamp)
.filter { it.questName == QuestName.ClearBarbarianCamp.value }
.filter { it.data1.toInt() == location.x.toInt() && it.data2.toInt() == location.y.toInt() } .filter { it.data1.toInt() == location.x.toInt() && it.data2.toInt() == location.y.toInt() }
val winningQuest = matchingQuests.filter { it.assignee == civInfo.civName }.firstOrNull() 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]. * Gets notified the city state [cityState] was just conquered by [attacker].
*/ */
fun cityStateConquered(cityState: Civilization, attacker: Civilization) { fun cityStateConquered(cityState: Civilization, attacker: Civilization) {
val matchingQuests = assignedQuests.asSequence() val matchingQuests = getAssignedQuestsOfName(QuestName.ConquerCityState)
.filter { it.questName == QuestName.ConquerCityState.value }
.filter { it.data1 == cityState.civName && it.assignee == attacker.civName } .filter { it.data1 == cityState.civName && it.assignee == attacker.civName }
for (quest in matchingQuests) for (quest in matchingQuests)
@ -530,8 +606,7 @@ class QuestManager : IsPartOfGameInfoSerialization {
* Gets notified the city state [cityState] was just bullied by [bully]. * Gets notified the city state [cityState] was just bullied by [bully].
*/ */
fun cityStateBullied(cityState: Civilization, bully: Civilization) { fun cityStateBullied(cityState: Civilization, bully: Civilization) {
val matchingQuests = assignedQuests.asSequence() val matchingQuests = getAssignedQuestsOfName(QuestName.BullyCityState)
.filter { it.questName == QuestName.BullyCityState.value }
.filter { it.data1 == cityState.civName && it.assignee == bully.civName} .filter { it.data1 == cityState.civName && it.assignee == bully.civName}
for (quest in matchingQuests) for (quest in matchingQuests)
@ -540,29 +615,30 @@ class QuestManager : IsPartOfGameInfoSerialization {
assignedQuests.removeAll(matchingQuests) assignedQuests.removeAll(matchingQuests)
// What idiots haha oh wait that's us // What idiots haha oh wait that's us
if (civInfo == cityState) { if (civ != cityState) return
// Revoke most quest types from the bully // Revoke most quest types from the bully
val revokedQuests = assignedQuests.asSequence() val revokedQuests = getAssignedQuestsFor(bully.civName)
.filter { it.assignee == bully.civName && (it.isIndividual() || it.questName == QuestName.Invest.value) } .filter { it.isIndividual() || it.questNameInstance == QuestName.Invest }
.toList()
assignedQuests.removeAll(revokedQuests) assignedQuests.removeAll(revokedQuests)
if (revokedQuests.count() > 0) if (revokedQuests.isEmpty()) return
bully.addNotification("[${civInfo.civName}] cancelled the quests they had given you because you demanded tribute from them.", bully.addNotification("[${civ.civName}] cancelled the quests they had given you because you demanded tribute from them.",
DiplomacyAction(civInfo.civName), DiplomacyAction(civ.civName),
NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest") NotificationCategory.Diplomacy, civ.civName, "OtherIcons/Quest")
}
} }
/** Gets notified when we are attacked, for war with major pseudo-quest */ /** Gets notified when we are attacked, for war with major pseudo-quest */
fun wasAttackedBy(attacker: Civilization) { fun wasAttackedBy(attacker: Civilization) {
// Set target number units to kill // Set target number units to kill
val totalMilitaryUnits = attacker.units.getCivUnits().count { !it.isCivilian() } val totalMilitaryUnits = attacker.units.getCivUnits().count { !it.isCivilian() }
val unitsToKill = max(3, totalMilitaryUnits / 4) val unitsToKill = (totalMilitaryUnits / 4).coerceAtMost(3)
unitsToKillForCiv[attacker.civName] = unitsToKill unitsToKillForCiv[attacker.civName] = unitsToKill
// Ask for assistance // Ask for assistance
val location = civInfo.getCapital(firstCityIfNoCapital = true)?.location val location = civ.getCapital(firstCityIfNoCapital = true)?.location
for (thirdCiv in civInfo.getKnownCivs()) { for (thirdCiv in civ.getKnownCivs()) {
if (!thirdCiv.isMajorCiv() || thirdCiv.isDefeated() || thirdCiv.isAtWarWith(civInfo)) if (!thirdCiv.isMajorCiv() || thirdCiv.isDefeated() || thirdCiv.isAtWarWith(civ))
continue continue
notifyAskForAssistance(thirdCiv, attacker.civName, unitsToKill, location) notifyAskForAssistance(thirdCiv, attacker.civName, unitsToKill, location)
} }
@ -570,11 +646,11 @@ class QuestManager : IsPartOfGameInfoSerialization {
private fun notifyAskForAssistance(assignee: Civilization, attackerName: String, unitsToKill: Int, location: Vector2?) { private fun notifyAskForAssistance(assignee: Civilization, attackerName: String, unitsToKill: Int, location: Vector2?) {
if (attackerName == assignee.civName) return // No "Hey Bob help us against Bob" 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! // Space relevant in template!
" Kill [$unitsToKill] of the attacker's military units and they will be immensely grateful." " 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` // 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 */ /** 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 if (!warWithMajorActive(killed)) return
// No credit if we're at war or haven't met // 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 // Make the map if we haven't already
if (unitsKilledFromCiv[killed.civName] == null) val unitsKilledFromCivEntry = unitsKilledFromCiv.getOrPut(killed.civName) { HashMap() }
unitsKilledFromCiv[killed.civName] = HashMap()
// Update kill count // Update kill count
val updatedKillCount = 1 + (unitsKilledFromCiv[killed.civName]!![killer.civName] ?: 0) val updatedKillCount = 1 + (unitsKilledFromCivEntry[killer.civName] ?: 0)
unitsKilledFromCiv[killed.civName]!![killer.civName] = updatedKillCount unitsKilledFromCivEntry[killer.civName] = updatedKillCount
// Quest complete? // Quest complete?
if (updatedKillCount >= unitsToKillForCiv[killed.civName]!!) { if (updatedKillCount >= unitsToKillForCiv[killed.civName]!!) {
killer.addNotification("[${civInfo.civName}] is deeply grateful for your assistance in the war against [${killed.civName}]!", killer.addNotification("[${civ.civName}] is deeply grateful for your assistance in the war against [${killed.civName}]!",
DiplomacyAction(civInfo.civName), NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest") DiplomacyAction(civ.civName), NotificationCategory.Diplomacy, civ.civName, "OtherIcons/Quest")
civInfo.getDiplomacyManager(killer).addInfluence(100f) // yikes civ.getDiplomacyManager(killer).addInfluence(100f) // yikes
endWarWithMajorQuest(killed) 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. */ /** Called when a major civ meets the city-state for the first time. Mainly for war with major pseudo-quest. */
fun justMet(otherCiv: Civilization) { fun justMet(otherCiv: Civilization) {
if (unitsToKillForCiv.isEmpty()) return if (unitsToKillForCiv.isEmpty()) return
val location = civInfo.getCapital(firstCityIfNoCapital = true)?.location val location = civ.getCapital(firstCityIfNoCapital = true)?.location
for ((attackerName, unitsToKill) in unitsToKillForCiv) for ((attackerName, unitsToKill) in unitsToKillForCiv)
notifyAskForAssistance(otherCiv, attackerName, unitsToKill, location) notifyAskForAssistance(otherCiv, attackerName, unitsToKill, location)
} }
/** Ends War with Major pseudo-quests that aren't relevant any longer */ /** Ends War with Major pseudo-quests that aren't relevant any longer */
private fun tryEndWarWithMajorQuests() { private fun tryEndWarWithMajorQuests() {
for (attacker in unitsToKillForCiv.keys.map { civInfo.gameInfo.getCivilization(it) }) { for (attacker in unitsToKillForCiv.keys.map { civ.gameInfo.getCivilization(it) }) {
if (civInfo.isDefeated() if (civ.isDefeated()
|| attacker.isDefeated() || attacker.isDefeated()
|| !civInfo.isAtWarWith(attacker)) { || !civ.isAtWarWith(attacker)) {
endWarWithMajorQuest(attacker) endWarWithMajorQuest(attacker)
} }
} }
} }
private fun endWarWithMajorQuest(attacker: Civilization) { 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 if (unitsKilledSoFar(attacker, thirdCiv) >= unitsToKill(attacker)) // Don't show the notification to the one who won the quest
continue continue
thirdCiv.addNotification("[${civInfo.civName}] no longer needs your assistance against [${attacker.civName}].", thirdCiv.addNotification("[${civ.civName}] no longer needs your assistance against [${attacker.civName}].",
DiplomacyAction(civInfo.civName), NotificationCategory.Diplomacy, civInfo.civName, "OtherIcons/Quest") DiplomacyAction(civ.civName), NotificationCategory.Diplomacy, civ.civName, "OtherIcons/Quest")
} }
unitsToKillForCiv.remove(attacker.civName) unitsToKillForCiv.remove(attacker.civName)
unitsKilledFromCiv.remove(attacker.civName) unitsKilledFromCiv.remove(attacker.civName)
@ -648,8 +723,7 @@ class QuestManager : IsPartOfGameInfoSerialization {
* Gets notified when given gold by [donorCiv]. * Gets notified when given gold by [donorCiv].
*/ */
fun receivedGoldGift(donorCiv: Civilization) { fun receivedGoldGift(donorCiv: Civilization) {
val matchingQuests = assignedQuests.asSequence() val matchingQuests = getAssignedQuestsOfName(QuestName.GiveGold)
.filter { it.questName == QuestName.GiveGold.value }
.filter { it.assignee == donorCiv.civName } .filter { it.assignee == donorCiv.civName }
for (quest in matchingQuests) for (quest in matchingQuests)
@ -663,23 +737,23 @@ class QuestManager : IsPartOfGameInfoSerialization {
*/ */
private fun getQuestWeight(questName: String): Float { private fun getQuestWeight(questName: String): Float {
var weight = 1f 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 if (personalityWeight != null) weight *= personalityWeight
val traitWeight = quest.weightForCityStateType[civInfo.cityStateType.name] val traitWeight = quest.weightForCityStateType[civ.cityStateType.name]
if (traitWeight != null) weight *= traitWeight if (traitWeight != null) weight *= traitWeight
return weight return weight
} }
//region get-quest-target //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 * to be destroyed
*/ */
private fun getBarbarianEncampmentForQuest(): Tile? { private fun getBarbarianEncampmentForQuest(): Tile? {
val encampments = civInfo.getCapital()!!.getCenterTile().getTilesInDistance(8) val encampments = civ.getCapital()!!.getCenterTile().getTilesInDistance(8)
.filter { it.improvement == Constants.barbarianEncampment }.toList() .filter { it.improvement == Constants.barbarianEncampment }.toList()
if (encampments.isNotEmpty()) 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. * 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 * 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. * if none exists, it returns null.
*/ */
private fun getResourceForQuest(challenger: Civilization): TileResource? { 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 ownedByMajorResources = challenger.detailedCivResources.map { it.resource }
val resourcesOnMap = civInfo.gameInfo.tileMap.values.asSequence().mapNotNull { it.resource }.distinct() val resourcesOnMap = civ.gameInfo.tileMap.values.asSequence().mapNotNull { it.resource }.distinct()
val viewableResourcesForChallenger = resourcesOnMap.map { civInfo.gameInfo.ruleset.tileResources[it]!! } val viewableResourcesForChallenger = resourcesOnMap.map { ruleset.tileResources[it]!! }
.filter { it.revealedBy == null || challenger.tech.isResearched(it.revealedBy!!) } .filter { it.revealedBy == null || challenger.tech.isResearched(it.revealedBy!!) }
val notOwnedResources = viewableResourcesForChallenger.filter { val notOwnedResources = viewableResourcesForChallenger.filter {
@ -715,18 +789,18 @@ class QuestManager : IsPartOfGameInfoSerialization {
} }
private fun getWonderToBuildForQuest(challenger: Civilization): Building? { private fun getWonderToBuildForQuest(challenger: Civilization): Building? {
val startingEra = civInfo.gameInfo.ruleset.eras[civInfo.gameInfo.gameParameters.startingEra]!! val startingEra = ruleset.eras[civ.gameInfo.gameParameters.startingEra]!!
val wonders = civInfo.gameInfo.ruleset.buildings.values val wonders = ruleset.buildings.values
.filter { building -> .filter { building ->
// Buildable wonder // Buildable wonder
building.isWonder building.isWonder
&& challenger.tech.isResearched(building) && 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 // Can't be disabled
&& building.name !in startingEra.startingObsoleteWonders && 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 // 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) } it.cityConstructions.getWorkDone(building.name) * 3 > it.cityConstructions.getRemainingWork(building.name) }
// Can't be a unique wonder // Can't be a unique wonder
&& building.uniqueTo == null && building.uniqueTo == null
@ -742,7 +816,7 @@ class QuestManager : IsPartOfGameInfoSerialization {
* Returns a random Natural Wonder not yet discovered by [challenger]. * Returns a random Natural Wonder not yet discovered by [challenger].
*/ */
private fun getNaturalWonderToFindForQuest(challenger: Civilization): String? { 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()) if (naturalWondersToFind.isNotEmpty())
return naturalWondersToFind.random() 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? { 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 challengerGreatPeople = challenger.units.getCivGreatPeople().map { it.baseUnit.getReplacedUnit(ruleset) }
val cityStateGreatPeople = civInfo.units.getCivGreatPeople().map { it.baseUnit.getReplacedUnit(ruleSet) } val cityStateGreatPeople = civ.units.getCivGreatPeople().map { it.baseUnit.getReplacedUnit(ruleset) }
val greatPeople = challenger.greatPeople.getGreatPeople() val greatPeople = challenger.greatPeople.getGreatPeople()
.map { it.getReplacedUnit(ruleSet) } .map { it.getReplacedUnit(ruleset) }
.distinct() .distinct()
.filterNot { challengerGreatPeople.contains(it) .filterNot { challengerGreatPeople.contains(it)
|| cityStateGreatPeople.contains(it) || cityStateGreatPeople.contains(it)
|| (it.hasUnique(UniqueType.HiddenWithoutReligion) && !civInfo.gameInfo.isReligionEnabled()) } || (it.hasUnique(UniqueType.HiddenWithoutReligion) && !civ.gameInfo.isReligionEnabled()) }
.toList() .toList()
if (greatPeople.isNotEmpty()) 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? { private fun getCityStateTarget(challenger: Civilization): Civilization? {
val closestProximity = civInfo.gameInfo.getAliveCityStates() val closestProximity = civ.gameInfo.getAliveCityStates()
.mapNotNull { civInfo.proximity[it.civName] }.filter { it != Proximity.None }.minByOrNull { it.ordinal } .mapNotNull { civ.proximity[it.civName] }.filter { it != Proximity.None }.minByOrNull { it.ordinal }
if (closestProximity == null || closestProximity == Proximity.Distant) // None close enough if (closestProximity == null || closestProximity == Proximity.Distant) // None close enough
return null return null
val validTargets = civInfo.getKnownCivs().filter { it.isCityState() && challenger.knows(it) val validTargets = civ.getKnownCivs().filter { it.isCityState() && challenger.knows(it)
&& civInfo.proximity[it.civName] == closestProximity } && civ.proximity[it.civName] == closestProximity }
return validTargets.toList().randomOrNull() 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! */ * Note: forgets after 20 turns has passed! */
private fun getMostRecentBully(): String? { 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 return bullies.maxByOrNull { it.getFlag(DiplomacyFlags.Bullied) }?.otherCivName
} }
//endregion //endregion
} }
class AssignedQuest(val questName: String = "", class AssignedQuest(
val questName: String = "",
val assigner: String = "", val assigner: String = "",
val assignee: String = "", val assignee: String = "",
val assignedOnTurn: Int = 0, val assignedOnTurn: Int = 0,
val data1: String = "", val data1: String = "",
val data2: String = "") : IsPartOfGameInfoSerialization { val data2: String = ""
) : IsPartOfGameInfoSerialization {
@Transient @Transient
lateinit var gameInfo: GameInfo private lateinit var gameInfo: GameInfo
fun isIndividual(): Boolean = !isGlobal() @Transient
fun isGlobal(): Boolean = gameInfo.ruleset.quests[questName]!!.isGlobal() private lateinit var questObject: Quest
@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)
fun getDescription(): String { val questNameInstance get() = questObject.questNameInstance
val quest = gameInfo.ruleset.quests[questName]!!
return quest.description.fillPlaceholders(data1) 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() { fun onClickAction() {
when (questName) { when (questNameInstance) {
QuestName.ClearBarbarianCamp.value -> { QuestName.ClearBarbarianCamp -> {
GUI.resetToWorldScreen() GUI.resetToWorldScreen()
GUI.getMap().setCenterPosition(Vector2(data1.toFloat(), data2.toFloat()), selectUnit = false) GUI.getMap().setCenterPosition(Vector2(data1.toFloat(), data2.toFloat()), selectUnit = false)
} }
QuestName.Route.value -> { QuestName.Route -> {
GUI.resetToWorldScreen() GUI.resetToWorldScreen()
GUI.getMap().setCenterPosition(gameInfo.getCivilization(assigner).getCapital()!!.location, selectUnit = false) 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 // Pick a luxury at random. Weight is reduced if the luxury has been picked before
val regionConditional = StateForConditionals(region = region) val regionConditional = StateForConditionals(region = region)
val modifiedWeights = candidateLuxuries.map { region.luxury = candidateLuxuries.randomWeighted {
val weightingUnique = it.getMatchingUniques(UniqueType.ResourceWeighting, regionConditional).firstOrNull() val weightingUnique = it.getMatchingUniques(UniqueType.ResourceWeighting, regionConditional).firstOrNull()
val relativeWeight = if (weightingUnique == null) 1f else weightingUnique.params[0].toFloat() val relativeWeight = if (weightingUnique == null) 1f else weightingUnique.params[0].toFloat()
relativeWeight / (1f + amountRegionsWithLuxury[it.name]!!) relativeWeight / (1f + amountRegionsWithLuxury[it.name]!!)
}.shuffled() }.name
region.luxury = candidateLuxuries.randomWeighted(modifiedWeights).name
amountRegionsWithLuxury[region.luxury!!] = amountRegionsWithLuxury[region.luxury]!! + 1 amountRegionsWithLuxury[region.luxury!!] = amountRegionsWithLuxury[region.luxury]!! + 1
} }
@ -150,15 +149,14 @@ object LuxuryResourcePlacementLogic {
} }
if (candidateLuxuries.isEmpty()) return@repeat if (candidateLuxuries.isEmpty()) return@repeat
val weights = candidateLuxuries.map { val luxury = candidateLuxuries.randomWeighted {
val weightingUnique = val weightingUnique =
it.getMatchingUniques(UniqueType.LuxuryWeightingForCityStates).firstOrNull() it.getMatchingUniques(UniqueType.LuxuryWeightingForCityStates).firstOrNull()
if (weightingUnique == null) if (weightingUnique == null)
1f 1f
else else
weightingUnique.params[0].toFloat() weightingUnique.params[0].toFloat()
} }.name
val luxury = candidateLuxuries.randomWeighted(weights).name
cityStateLuxuries.add(luxury) cityStateLuxuries.add(luxury)
amountRegionsWithLuxury[luxury] = 1 amountRegionsWithLuxury[luxury] = 1
} }

View File

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

View File

@ -1,7 +1,7 @@
package com.unciv.models.ruleset package com.unciv.models.ruleset
import com.unciv.logic.civilization.Civilization
import com.unciv.models.stats.INamed import com.unciv.models.stats.INamed
import com.unciv.logic.civilization.Civilization // for Kdoc
enum class QuestName(val value: String) { enum class QuestName(val value: String) {
Route("Route"), Route("Route"),
@ -22,6 +22,10 @@ enum class QuestName(val value: String) {
DenounceCiv("Denounce Civilization"), DenounceCiv("Denounce Civilization"),
SpreadReligion("Spread Religion"), SpreadReligion("Spread Religion"),
None("") None("")
;
companion object {
fun find(value: String) = values().firstOrNull { it.value == value } ?: None
}
} }
enum class QuestType { enum class QuestType {
@ -33,12 +37,14 @@ enum class QuestType {
// Notes: This is **not** `IsPartOfGameInfoSerialization`, only Ruleset. // Notes: This is **not** `IsPartOfGameInfoSerialization`, only Ruleset.
// Saves contain [QuestManager]s instead, which contain lists of [AssignedQuest] instances. // Saves contain [QuestManager]s instead, which contain lists of [AssignedQuest] instances.
// These are matched to this Quest **by name**. // 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 { 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 = "" 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 */ /** Description of the quest shown to players */
var description: String = "" var description: String = ""

View File

@ -23,6 +23,13 @@ fun <T> List<T>.randomWeighted(weights: List<Float>, random: Random = Random): T
return this.last() 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 /** 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 * 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) val diplomaticMarriageButton = getDiplomaticMarriageButton(otherCiv)
if (diplomaticMarriageButton != null) diplomacyTable.add(diplomaticMarriageButton).row() 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.addSeparator()
diplomacyTable.add(getQuestTable(assignedQuest)).row() diplomacyTable.add(getQuestTable(assignedQuest)).row()
} }
@ -464,8 +464,8 @@ class CityStateDiplomacyTable(private val diplomacyScreen: DiplomacyScreen) {
if (quest.duration > 0) if (quest.duration > 0)
questTable.add("[${remainingTurns}] turns remaining".toLabel()).row() questTable.add("[${remainingTurns}] turns remaining".toLabel()).row()
if (quest.isGlobal()) { if (quest.isGlobal()) {
val leaderString = viewingCiv.gameInfo.getCivilization(assignedQuest.assigner).questManager.getLeaderStringForQuest(assignedQuest.questName) val leaderString = viewingCiv.gameInfo.getCivilization(assignedQuest.assigner).questManager.getScoreStringForGlobalQuest(assignedQuest)
if (leaderString != "") if (leaderString.isNotEmpty())
questTable.add(leaderString.toLabel()).row() questTable.add(leaderString.toLabel()).row()
} }

View File

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