City State quests (#3183)

* City State quests

* Flag to log two civ ever been friends
* Utility functions in GameInfo
* Created Diplomacy Action for notifications
* Utility functions for map
* Can be specified a custom color for surroundWithCircle
* Translation placeholder utility
* Added Quest model
* Utility function: number of researched technologies

* Image atlas rebuilt

* Localization

* Updated DiplomaticFlags and added EverBeenFriends

Slightly reworked nextTurnFlags() for code clarity and introduced the new flag EverBeenFriends that is set as soon as two civilizations are at least friends. It never expires.

* Removed quests not implemented yet from json
This commit is contained in:
Federico Luongo 2020-09-29 22:26:50 +02:00 committed by GitHub
parent 847abf31d1
commit adaee7e7ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1127 additions and 471 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 899 KiB

After

Width:  |  Height:  |  Size: 899 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

After

Width:  |  Height:  |  Size: 487 KiB

View File

@ -0,0 +1,113 @@
[
{
"name": "Route",
"description": "Build a road to connect your capital to our city.",
"influence": 50
},
/*
{
"name": "Kill Camp",
"description": "We feel threatened by a Barbarian Camp near our city. Please take care of it.",
"type": "Global",
"influence": 50,
"minimumCivs": 1
},
{
"name": "Connect Resource",
"description": "In order to make our civilizations stronger, connect [Resource] to your trade network."
},*/
{
"name": "Construct Wonder",
"description": "We recommend you to start building [Wonder] to show the whole world your civilization strength."
},/*
{
"name": "Great Person",
"description": "Great People can change the course of a Civilization! You will be rewarded for acquiring a new [Great Person]."
},
{
"name": "Kill City State",
"description": "You will be rewarded for destroying the city state of [Target]!",
"influence": 80
},
{
"name": "Find Player",
"description": "You have yet to discover where [Civilization] set up their cities. You will be rewarded for finding their territories.",
"influence": 35
},
{
"name": "Find Natural Wonder",
"description": "Send your best explorers on a quest to discover Natural Wonders. Nobody knows the location of [Natural Wonder] yet."
},*/
/* G&K */
/*
{
"name": "Give Gold",
"description": "",
"influence": 20,
"duration": 30
},
{
"name": "Pledge to Protect",
"description": "",
"influence": 20,
"duration": 30
},
*/
/*
{
"name": "Contest Culture",
"description": "The civilization with the largest Culture growth will gain a reward.",
"type": "Global",
"duration": 30,
"minimumCivs": 3
},*/
/*
{
"name": "Contest Faith",
"description": "",
"type": "Global",
"duration": 30,
"minimumCivs": 3
},*/
{
"name": "Contest Techs",
"description": "The civilization with the largest number of new Technologies researched will gain a reward.",
"type": "Global",
"duration": 30,
"minimumCivs": 3
},
/*
{
"name": "Invest",
"description": "",
"type": "Global",
"influence": 0,
"duration": 30,
"minimumCivs": 2
},
{
"name": "Bully City State",
"description": ""
"duration": 30
},
{
"name": "Denounce Civilization",
"description": "",
"duration": 30
}
*/
/*
{
"name": "Spread Religion",
"description": ""
},
*/
/* BNW */
/*
{
"name": "Trade Route",
"description": ""
}
*/
]

View File

@ -90,6 +90,9 @@ Favorable =
Friend = Friend =
Ally = Ally =
[questName] (+[influenceAmount] influence) =
Remaining [remainingTurns] turns =
## Diplomatic modifiers ## Diplomatic modifiers
You declared war on us! = You declared war on us! =
@ -436,7 +439,8 @@ Our proposed trade request is no longer relevant! =
[building] has provided [amount] Gold! = [building] has provided [amount] Gold! =
[civName] has stolen your territory! = [civName] has stolen your territory! =
Clearing a [forest] has created [amount] Production for [cityName] = Clearing a [forest] has created [amount] Production for [cityName] =
[civName] assigned you a new quest: [questName]. =
[civName] rewarded you with [influence] influence for completing the [questName] quest. =
# World Screen UI # World Screen UI
@ -916,3 +920,24 @@ in this city =
in every city = in every city =
in capital = in capital =
# Quests
Route =
Build a road to connect your capital to our city. =
Kill Camp =
We feel threatened by a Barbarian Camp near our city. Please take care of it. =
Connect Resource =
In order to make our civilizations stronger, connect [Resource] to your trade network. =
Construct Wonder =
We recommend you to start building [Wonder] to show the whole world your civilization strength. =
Great Person =
Great People can change the course of a Civilization! You will be rewarded for acquiring a new [Great Person]. =
Kill City State =
You will be rewarded for destroying the city state of [Target]! =
Find Player =
You have yet to discover where [Civilization] set up their cities. You will be rewarded for finding their territories. =
Find Natural Wonder =
Send your best explorers on a quest to discover Natural Wonders. Nobody knows the location of [Natural Wonder] yet. =
Contest Culture =
The civilization with the largest Culture growth will gain a reward. =
Contest Techs =
The civilization with the largest number of new Technologies researched will gain a reward. =

View File

@ -73,6 +73,8 @@ class GameInfo {
fun getBarbarianCivilization() = getCivilization(Constants.barbarians) fun getBarbarianCivilization() = getCivilization(Constants.barbarians)
fun getDifficulty() = difficultyObject fun getDifficulty() = difficultyObject
fun getCities() = civilizations.flatMap { it.cities } fun getCities() = civilizations.flatMap { it.cities }
fun getAliveCityStates() = civilizations.filter { it.isAlive() && it.isCityState() }
fun getAliveMajorCivs() = civilizations.filter { it.isAlive() && it.isMajorCiv() }
//endregion //endregion
fun nextTurn() { fun nextTurn() {

View File

@ -3,7 +3,6 @@ package com.unciv.logic.civilization
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.math.Vector2
import com.unciv.Constants import com.unciv.Constants
import com.unciv.JsonParser
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.GameInfo import com.unciv.logic.GameInfo
import com.unciv.logic.UncivShowableException import com.unciv.logic.UncivShowableException
@ -12,12 +11,14 @@ import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.diplomacy.DiplomacyManager import com.unciv.logic.civilization.diplomacy.DiplomacyManager
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
import com.unciv.logic.map.MapUnit import com.unciv.logic.map.MapUnit
import com.unciv.logic.map.TileInfo import com.unciv.logic.map.TileInfo
import com.unciv.logic.trade.TradeEvaluation import com.unciv.logic.trade.TradeEvaluation
import com.unciv.logic.trade.TradeRequest import com.unciv.logic.trade.TradeRequest
import com.unciv.models.ruleset.* import com.unciv.models.ruleset.*
import com.unciv.models.ruleset.tile.ResourceSupplyList import com.unciv.models.ruleset.tile.ResourceSupplyList
import com.unciv.models.ruleset.tile.TileResource
import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.stats.Stats import com.unciv.models.stats.Stats
import com.unciv.models.translations.equalsPlaceholderText import com.unciv.models.translations.equalsPlaceholderText
@ -56,6 +57,7 @@ class CivilizationInfo {
var civName = "" var civName = ""
var tech = TechManager() var tech = TechManager()
var policies = PolicyManager() var policies = PolicyManager()
var questManager = QuestManager()
var goldenAges = GoldenAgeManager() var goldenAges = GoldenAgeManager()
var greatPeople = GreatPersonManager() var greatPeople = GreatPersonManager()
var victoryManager=VictoryManager() var victoryManager=VictoryManager()
@ -89,6 +91,7 @@ class CivilizationInfo {
toReturn.civName = civName toReturn.civName = civName
toReturn.tech = tech.clone() toReturn.tech = tech.clone()
toReturn.policies = policies.clone() toReturn.policies = policies.clone()
toReturn.questManager = questManager.clone()
toReturn.goldenAges = goldenAges.clone() toReturn.goldenAges = goldenAges.clone()
toReturn.greatPeople = greatPeople.clone() toReturn.greatPeople = greatPeople.clone()
toReturn.victoryManager = victoryManager.clone() toReturn.victoryManager = victoryManager.clone()
@ -133,6 +136,8 @@ class CivilizationInfo {
fun isCityState(): Boolean = nation.isCityState() fun isCityState(): Boolean = nation.isCityState()
fun getCityStateType(): CityStateType = nation.cityStateType!! fun getCityStateType(): CityStateType = nation.cityStateType!!
fun isMajorCiv() = nation.isMajorCiv() fun isMajorCiv() = nation.isMajorCiv()
fun isAlive(): Boolean = !isDefeated()
fun hasEverBeenFriendWith(otherCiv: CivilizationInfo): Boolean = getDiplomacyManager(otherCiv).everBeenFriends()
fun victoryType(): VictoryType { fun victoryType(): VictoryType {
if(gameInfo.gameParameters.victoryTypes.size==1) if(gameInfo.gameParameters.victoryTypes.size==1)
@ -160,6 +165,8 @@ class CivilizationInfo {
return newResourceSupplyList return newResourceSupplyList
} }
fun isCapitalConnectedToCity(city: CityInfo): Boolean = citiesConnectedToCapitalToMediums.keys.contains(city)
/** /**
* Returns a dictionary of ALL resource names, and the amount that the civ has of each * Returns a dictionary of ALL resource names, and the amount that the civ has of each
@ -367,11 +374,15 @@ class CivilizationInfo {
fun setTransients() { fun setTransients() {
goldenAges.civInfo = this goldenAges.civInfo = this
policies.civInfo = this policies.civInfo = this
if(policies.adoptedPolicies.size>0 && policies.numberOfAdoptedPolicies == 0) if(policies.adoptedPolicies.size>0 && policies.numberOfAdoptedPolicies == 0)
policies.numberOfAdoptedPolicies = policies.adoptedPolicies.count { !it.endsWith("Complete") } policies.numberOfAdoptedPolicies = policies.adoptedPolicies.count { !it.endsWith("Complete") }
policies.setTransients() policies.setTransients()
questManager.civInfo = this
questManager.setTransients()
if(citiesCreated==0 && cities.any()) if(citiesCreated==0 && cities.any())
citiesCreated = cities.filter { it.name in nation.cities }.count() citiesCreated = cities.filter { it.name in nation.cities }.count()
@ -438,6 +449,9 @@ class CivilizationInfo {
policies.endTurn(nextTurnStats.culture.toInt()) policies.endTurn(nextTurnStats.culture.toInt())
if (isCityState())
questManager.endTurn()
// disband units until there are none left OR the gold values are normal // disband units until there are none left OR the gold values are normal
if (!isBarbarian() && gold < -100 && nextTurnStats.gold.toInt() < 0) { if (!isBarbarian() && gold < -100 && nextTurnStats.gold.toInt() < 0) {
for (i in 1 until (gold / -100)) { for (i in 1 until (gold / -100)) {

View File

@ -4,6 +4,7 @@ import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.math.Vector2
import com.unciv.ui.cityscreen.CityScreen import com.unciv.ui.cityscreen.CityScreen
import com.unciv.ui.pickerscreens.TechPickerScreen import com.unciv.ui.pickerscreens.TechPickerScreen
import com.unciv.ui.trade.DiplomacyScreen
import com.unciv.ui.worldscreen.WorldScreen import com.unciv.ui.worldscreen.WorldScreen
/** /**
@ -54,4 +55,12 @@ data class CityAction(val city: Vector2 = Vector2.Zero): NotificationAction {
} }
} }
}
data class DiplomacyAction(val otherCivName: String = ""): NotificationAction {
override fun execute(worldScreen: WorldScreen) {
val screen = DiplomacyScreen(worldScreen.viewingCiv)
screen.updateRightSide(worldScreen.gameInfo.getCivilization(otherCivName))
worldScreen.game.setScreen(screen)
}
} }

View File

@ -0,0 +1,378 @@
package com.unciv.logic.civilization
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.math.Vector2
import com.unciv.UncivGame
import com.unciv.logic.GameInfo
import com.unciv.logic.map.TileInfo
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.Quest
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.TileResource
import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.translations.equalsPlaceholderText
import com.unciv.models.translations.fillPlaceholders
import kotlin.math.max
import kotlin.random.Random
class QuestManager {
companion object {
const val UNSET = -1
const val GLOBAL_QUEST_FIRST_POSSIBLE_TURN = 30
const val INDIVIDUAL_QUEST_FIRST_POSSIBLE_TURN = 30
const val GLOBAL_QUEST_FIRST_POSSIBLE_TURN_RAND = 20
const val INDIVIDUAL_QUEST_FIRST_POSSIBLE_TURN_RAND = 20
const val GLOBAL_QUEST_MIN_TURNS_BETWEEN = 40
const val INDIVIDUAL_QUEST_MIN_TURNS_BETWEEN = 20
const val GLOBAL_QUEST_RAND_TURNS_BETWEEN = 25
const val INDIVIDUAL_QUEST_RAND_TURNS_BETWEEN = 25
const val GLOBAL_QUEST_MAX_ACTIVE = 1
const val INDIVIDUAL_QUEST_MAX_ACTIVE = 2
}
/** Civilization object holding and dispatching quests */
@Transient
lateinit var civInfo: CivilizationInfo
/** List of active quests, both global and individual ones*/
var assignedQuests: ArrayList<AssignedQuest> = ArrayList()
/** Number of turns left before starting new global quest */
private var globalQuestCountdown: Int = UNSET
/** Number of turns left before this city state can start a new individual quest */
private var individualQuestCountdown: HashMap<String, Int> = HashMap()
/** Returns [true] if [civInfo] have active quests for [challenger] */
fun haveQuestsFor(challenger: CivilizationInfo): Boolean = assignedQuests.any { it.assignee == challenger.civName }
fun clone(): QuestManager {
val toReturn = QuestManager()
toReturn.globalQuestCountdown = globalQuestCountdown
toReturn.individualQuestCountdown.putAll(individualQuestCountdown)
toReturn.assignedQuests.addAll(assignedQuests)
return toReturn
}
fun setTransients() {
for (quest in assignedQuests)
quest.gameInfo = civInfo.gameInfo
}
fun endTurn() {
if (civInfo.isDefeated()) {
assignedQuests.clear()
individualQuestCountdown.clear()
globalQuestCountdown = UNSET
return
}
seedGlobalQuestCountdown()
seedIndividualQuestsCountdown()
decrementQuestCountdowns()
handleGlobalQuests()
handleIndividualQuests()
tryStartNewGlobalQuest()
tryStartNewIndividualQuests()
}
private fun decrementQuestCountdowns() {
if (globalQuestCountdown > 0)
globalQuestCountdown -= 1
for (entry in individualQuestCountdown)
if (entry.value > 0)
entry.setValue(entry.value - 1)
}
private fun seedGlobalQuestCountdown() {
if (civInfo.gameInfo.turns < GLOBAL_QUEST_FIRST_POSSIBLE_TURN)
return
if (globalQuestCountdown != UNSET)
return
val countdown =
if (civInfo.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.gameParameters.gameSpeed.modifier).toInt()
}
private fun seedIndividualQuestsCountdown() {
if (civInfo.gameInfo.turns < INDIVIDUAL_QUEST_FIRST_POSSIBLE_TURN)
return
val majorCivs = civInfo.gameInfo.getAliveMajorCivs()
for (majorCiv in majorCivs)
if (!individualQuestCountdown.containsKey(majorCiv.civName) || individualQuestCountdown[majorCiv.civName] == UNSET)
seedIndividualQuestsCountdown(majorCiv)
}
private fun seedIndividualQuestsCountdown(challenger: CivilizationInfo) {
val countdown: Int =
if (civInfo.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.gameParameters.gameSpeed.modifier).toInt()
}
private fun tryStartNewGlobalQuest() {
if (globalQuestCountdown != 0)
return
if (assignedQuests.count { it.isGlobal() } >= GLOBAL_QUEST_MAX_ACTIVE)
return
val globalQuests = civInfo.gameInfo.ruleSet.quests.values.filter { it.isGlobal() }
val majorCivs = civInfo.getKnownCivs().filter { it.isMajorCiv() && !it.isAtWarWith(civInfo) }
val assignableQuests = ArrayList<Quest>()
for (quest in globalQuests) {
val numberValidMajorCivs = majorCivs.count { civ -> isQuestValid(quest, civ) }
if (numberValidMajorCivs >= quest.minimumCivs)
assignableQuests.add(quest)
}
//TODO: quest probabilities should change based on City State personality and traits
if (assignableQuests.isNotEmpty()) {
val quest = assignableQuests.random()
val assignees = civInfo.gameInfo.getAliveMajorCivs().filter { !it.isAtWarWith(civInfo) && isQuestValid(quest, it) }
assignNewQuest(quest, assignees)
globalQuestCountdown = UNSET
}
}
private fun tryStartNewIndividualQuests() {
for ((challengerName, countdown) in individualQuestCountdown) {
val challenger = civInfo.gameInfo.getCivilization(challengerName)
if (countdown != 0)
return
if (assignedQuests.count { it.assignee == challenger.civName && it.isIndividual() } >= INDIVIDUAL_QUEST_MAX_ACTIVE)
return
val assignableQuests = civInfo.gameInfo.ruleSet.quests.values.filter { it.isIndividual() && isQuestValid(it, challenger) }
//TODO: quest probabilities should change based on City State personality and traits
if (assignableQuests.isNotEmpty()) {
val quest = assignableQuests.random()
val assignees = arrayListOf(challenger)
assignNewQuest(quest, assignees)
}
}
}
private fun handleGlobalQuests() {
val globalQuestsExpired = assignedQuests.filter { it.isGlobal() && it.isExpired() }.map { it.questName }.distinct()
for (globalQuestName in globalQuestsExpired)
handleGlobalQuest(globalQuestName)
}
private fun handleGlobalQuest(questName: String) {
val quests = assignedQuests.filter { it.questName == questName }
if (quests.isEmpty())
return
val topScore = quests.map { getScoreForQuest(it) }.max()!!
for (quest in quests) {
if (getScoreForQuest(quest) >= topScore)
giveReward(quest)
}
assignedQuests.removeAll(quests)
}
private fun handleIndividualQuests() {
val toRemove = ArrayList<AssignedQuest>()
for (assignedQuest in assignedQuests.filter { it.isIndividual() }) {
val shouldRemove = handleIndividualQuest(assignedQuest)
if (shouldRemove)
toRemove.add(assignedQuest)
}
assignedQuests.removeAll(toRemove)
}
/** 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)
// One of the civs is defeated, or they started a war: remove quest
if (!canAssignAQuestTo(assignee))
return true
if (isComplete(assignedQuest)) {
giveReward(assignedQuest)
return true
}
if (isObsolete(assignedQuest))
return true
if (assignedQuest.isExpired())
return true
return false
}
private fun assignNewQuest(quest: Quest, assignees: Iterable<CivilizationInfo>) {
val turn = civInfo.gameInfo.turns
for (assignee in assignees) {
var data1 = ""
var data2 = ""
when (quest.name) {
"Construct Wonder" -> data1 = getWonderToBuildForQuest(assignee)!!.name
"Contest Techs" -> data1 = assignee.tech.getNumberOfTechsResearched().toString()
}
val newQuest = AssignedQuest(
questName = quest.name,
assigner = civInfo.civName,
assignee = assignee.civName,
assignedOnTurn = turn,
data1 = data1,
data2 = data2
)
newQuest.gameInfo = civInfo.gameInfo
assignedQuests.add(newQuest)
assignee.addNotification("[${civInfo.civName}] assigned you a new quest: [${quest.name}].", Color.GOLD, DiplomacyAction(civInfo.civName))
if (quest.isIndividual())
individualQuestCountdown[assignee.civName] = UNSET
}
}
/** Returns [true] if [civInfo] can assign a quest to [challenger] */
private fun canAssignAQuestTo(challenger: CivilizationInfo): Boolean {
return !challenger.isDefeated() && challenger.isMajorCiv() &&
civInfo.knows(challenger) && !civInfo.isAtWarWith(challenger)
}
/** Returns [true] if the [quest] can be assigned to [challenger] */
private fun isQuestValid(quest: Quest, challenger: CivilizationInfo): Boolean {
if (!canAssignAQuestTo(challenger))
return false
if (assignedQuests.any { it.assignee == challenger.civName && it.questName == quest.name })
return false
return when (quest.name) {
"Route" -> civInfo.hasEverBeenFriendWith(challenger) && !civInfo.isCapitalConnectedToCity(challenger.getCapital())
"Construct Wonder" -> civInfo.hasEverBeenFriendWith(challenger) && getWonderToBuildForQuest(challenger) != null
else -> true
}
}
/** 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) {
"Route" -> civInfo.isCapitalConnectedToCity(assignee.getCapital())
"Construct Wonder" -> assignee.cities.any { it.cityConstructions.isBuilt(assignedQuest.data1) }
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) {
"Construct Wonder" -> civInfo.gameInfo.getCities().any { it.civInfo != assignee && it.cityConstructions.isBuilt(assignedQuest.data1) }
else -> false
}
}
/** Increments [assignedQuest.assignee] influence on [civInfo] and adds a [Notification] */
private fun giveReward(assignedQuest: AssignedQuest) {
val rewardInfluence = civInfo.gameInfo.ruleSet.quests[assignedQuest.questName]!!.influece
val assignee = civInfo.gameInfo.getCivilization(assignedQuest.assignee)
civInfo.getDiplomacyManager(assignedQuest.assignee).influence += rewardInfluence
if (rewardInfluence > 0)
assignee.addNotification("[${civInfo.civName}] rewarded you with [${rewardInfluence.toInt()}] influence for completing the [${assignedQuest.questName}] quest.", civInfo.getCapital().location, Color.GOLD)
}
/** Returns the score for the [assignedQuest] */
private fun getScoreForQuest(assignedQuest: AssignedQuest): Int {
val assignee = civInfo.gameInfo.getCivilization(assignedQuest.assignee)
return when (assignedQuest.questName) {
"Contest Techs" -> assignee.tech.getNumberOfTechsResearched() - assignedQuest.data1.toInt()
else -> 0
}
}
//region get-quest-target
private fun getWonderToBuildForQuest(challenger: CivilizationInfo): Building? {
val wonders = civInfo.gameInfo.ruleSet.buildings.values
.filter { building ->
building.isWonder &&
(building.requiredTech == null || challenger.tech.isResearched(building.requiredTech!!)) &&
civInfo.gameInfo.getCities().none { it.cityConstructions.isBuilt(building.name) }
}
if (wonders.isNotEmpty())
return wonders.random()
return null
}
//endregion
}
class AssignedQuest(val questName: String = "",
val assigner: String = "",
val assignee: String = "",
val assignedOnTurn: Int = 0,
val data1: String = "",
val data2: String = "") {
@Transient
lateinit var gameInfo: GameInfo
fun isIndividual(): Boolean = !isGlobal()
fun isGlobal(): Boolean = gameInfo.ruleSet.quests[questName]!!.isGlobal()
fun doesExpire(): Boolean = gameInfo.ruleSet.quests[questName]!!.duration > 0
fun isExpired(): Boolean = doesExpire() && getRemainingTurns() == 0
fun getDuration(): Int = (gameInfo.gameParameters.gameSpeed.modifier * gameInfo.ruleSet.quests[questName]!!.duration).toInt()
fun getRemainingTurns(): Int = max(0, (assignedOnTurn + getDuration()) - gameInfo.turns)
fun getDescription(): String {
val quest = gameInfo.ruleSet.quests[questName]!!
return quest.description.fillPlaceholders(data1)
}
fun onClickAction() {
val game = UncivGame.Current
when (questName) {
"Route" -> {
game.setWorldScreen()
game.worldScreen.mapHolder.setCenterPosition(gameInfo.getCivilization(assigner).getCapital().location, selectUnit = false)
}
}
}
}

View File

@ -59,6 +59,8 @@ class TechManager {
return toReturn return toReturn
} }
fun getNumberOfTechsResearched(): Int = techsResearched.size
fun getRuleset() = civInfo.gameInfo.ruleSet fun getRuleset() = civInfo.gameInfo.ruleSet
fun costOfTech(techName: String): Int { fun costOfTech(techName: String): Int {

View File

@ -33,7 +33,8 @@ enum class DiplomacyFlags{
SettledCitiesNearUs, SettledCitiesNearUs,
AgreedToNotSettleNearUs, AgreedToNotSettleNearUs,
IgnoreThemSettlingNearUs, IgnoreThemSettlingNearUs,
ProvideMilitaryUnit ProvideMilitaryUnit,
EverBeenFriends
} }
enum class DiplomaticModifiers{ enum class DiplomaticModifiers{
@ -285,6 +286,16 @@ class DiplomacyManager() {
nextTurnFlags() nextTurnFlags()
if (civInfo.isCityState() && !otherCiv().isCityState()) if (civInfo.isCityState() && !otherCiv().isCityState())
nextTurnCityStateInfluence() nextTurnCityStateInfluence()
updateEverBeenFriends()
}
/** True when the two civs have been friends in the past */
fun everBeenFriends(): Boolean = hasFlag(DiplomacyFlags.EverBeenFriends)
/** Set [DiplomacyFlags.EverBeenFriends] if the two civilization are currently at least friends */
private fun updateEverBeenFriends() {
if (relationshipLevel() >= RelationshipLevel.Friend && !everBeenFriends())
setFlag(DiplomacyFlags.EverBeenFriends, -1)
} }
private fun nextTurnCityStateInfluence() { private fun nextTurnCityStateInfluence() {
@ -310,21 +321,35 @@ class DiplomacyManager() {
} }
private fun nextTurnFlags() { private fun nextTurnFlags() {
for (flag in flagsCountdown.keys.toList()) { loop@ for (flag in flagsCountdown.keys.toList()) {
if (flag == DiplomacyFlags.ResearchAgreement.name){ // No need to decrement negative countdown flags: they do not expire
if (flagsCountdown[flag]!! > 0)
flagsCountdown[flag] = flagsCountdown[flag]!! - 1
// At the end of every turn
if (flag == DiplomacyFlags.ResearchAgreement.name)
totalOfScienceDuringRA += civInfo.statsForNextTurn.science.toInt() totalOfScienceDuringRA += civInfo.statsForNextTurn.science.toInt()
}
flagsCountdown[flag] = flagsCountdown[flag]!! - 1 // Only when flag is expired
if (flagsCountdown[flag] == 0) { if (flagsCountdown[flag] == 0) {
if (flag == DiplomacyFlags.ResearchAgreement.name && !otherCivDiplomacy().hasFlag(DiplomacyFlags.ResearchAgreement)) when (flag) {
sciencefromResearchAgreement() DiplomacyFlags.ResearchAgreement.name -> {
if (flag == DiplomacyFlags.ProvideMilitaryUnit.name && civInfo.cities.isEmpty() || otherCiv().cities.isEmpty()) if (!otherCivDiplomacy().hasFlag(DiplomacyFlags.ResearchAgreement))
continue sciencefromResearchAgreement()
}
DiplomacyFlags.ProvideMilitaryUnit.name -> {
// Do not unset the flag
if (civInfo.cities.isEmpty() || otherCiv().cities.isEmpty())
continue@loop
else
civInfo.giftMilitaryUnitTo(otherCiv())
}
DiplomacyFlags.AgreedToNotSettleNearUs.name -> {
addModifier(DiplomaticModifiers.FulfilledPromiseToNotSettleCitiesNearUs, 10f)
}
}
flagsCountdown.remove(flag) flagsCountdown.remove(flag)
if (flag == DiplomacyFlags.AgreedToNotSettleNearUs.name)
addModifier(DiplomaticModifiers.FulfilledPromiseToNotSettleCitiesNearUs, 10f)
else if (flag == DiplomacyFlags.ProvideMilitaryUnit.name)
civInfo.giftMilitaryUnitTo(otherCiv())
} }
} }
} }

View File

@ -0,0 +1,35 @@
package com.unciv.models.ruleset
import com.unciv.models.stats.INamed
enum class QuestType {
Individual,
Global
}
/** [Quest] class holds all functionality relative to a quest */
class Quest : INamed {
/** Unique identifier name of the quest, it is also shown */
override var name: String = ""
/** Descrption of the quest shown to players */
var description: String = ""
/** [QuestType]: it is either Individual or Global */
var type: QuestType = QuestType.Individual
/** Influence reward gained on quest completion */
var influece: Float = 40f
/** Maximum number of turns to complete the quest, 0 if there's no turn limit */
var duration: Int = 0
/**Minimum number of [CivInfo] needed to start the quest. It is meaningful only for [QuestType.Global]
* quests [type]. */
var minimumCivs: Int = 1
/** Checks if [this] is a Global quest */
fun isGlobal(): Boolean = type == QuestType.Global
fun isIndividual(): Boolean = !isGlobal()
}

View File

@ -1,11 +1,8 @@
package com.unciv.models.ruleset package com.unciv.models.ruleset
import com.badlogic.gdx.Files
import com.badlogic.gdx.Gdx import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.files.FileHandle
import com.unciv.Constants
import com.unciv.JsonParser import com.unciv.JsonParser
import com.unciv.UncivGame
import com.unciv.logic.UncivShowableException import com.unciv.logic.UncivShowableException
import com.unciv.models.metadata.BaseRuleset import com.unciv.models.metadata.BaseRuleset
import com.unciv.models.metadata.GameParameters import com.unciv.models.metadata.GameParameters
@ -17,7 +14,6 @@ import com.unciv.models.ruleset.tile.TileResource
import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.ruleset.unit.Promotion import com.unciv.models.ruleset.unit.Promotion
import com.unciv.models.stats.INamed import com.unciv.models.stats.INamed
import java.lang.StringBuilder
import kotlin.collections.set import kotlin.collections.set
object ModOptionsConstants { object ModOptionsConstants {
@ -46,6 +42,7 @@ class Ruleset {
val units = LinkedHashMap<String, BaseUnit>() val units = LinkedHashMap<String, BaseUnit>()
val unitPromotions = LinkedHashMap<String, Promotion>() val unitPromotions = LinkedHashMap<String, Promotion>()
val nations = LinkedHashMap<String, Nation>() val nations = LinkedHashMap<String, Nation>()
val quests = LinkedHashMap<String, Quest>()
val policyBranches = LinkedHashMap<String, PolicyBranch>() val policyBranches = LinkedHashMap<String, PolicyBranch>()
val difficulties = LinkedHashMap<String, Difficulty>() val difficulties = LinkedHashMap<String, Difficulty>()
val mods = LinkedHashSet<String>() val mods = LinkedHashSet<String>()
@ -70,6 +67,7 @@ class Ruleset {
difficulties.putAll(ruleset.difficulties) difficulties.putAll(ruleset.difficulties)
nations.putAll(ruleset.nations) nations.putAll(ruleset.nations)
policyBranches.putAll(ruleset.policyBranches) policyBranches.putAll(ruleset.policyBranches)
quests.putAll(ruleset.quests)
technologies.putAll(ruleset.technologies) technologies.putAll(ruleset.technologies)
for (techToRemove in ruleset.modOptions.techsToRemove) technologies.remove(techToRemove) for (techToRemove in ruleset.modOptions.techsToRemove) technologies.remove(techToRemove)
terrains.putAll(ruleset.terrains) terrains.putAll(ruleset.terrains)
@ -87,6 +85,7 @@ class Ruleset {
difficulties.clear() difficulties.clear()
nations.clear() nations.clear()
policyBranches.clear() policyBranches.clear()
quests.clear()
technologies.clear() technologies.clear()
buildings.clear() buildings.clear()
terrains.clear() terrains.clear()
@ -139,6 +138,9 @@ class Ruleset {
val promotionsFile = folderHandle.child("UnitPromotions.json") val promotionsFile = folderHandle.child("UnitPromotions.json")
if (promotionsFile.exists()) unitPromotions += createHashmap(jsonParser.getFromJson(Array<Promotion>::class.java, promotionsFile)) if (promotionsFile.exists()) unitPromotions += createHashmap(jsonParser.getFromJson(Array<Promotion>::class.java, promotionsFile))
val questsFile = folderHandle.child("Quests.json")
if (questsFile.exists()) quests += createHashmap(jsonParser.getFromJson(Array<Quest>::class.java, questsFile))
val policiesFile = folderHandle.child("Policies.json") val policiesFile = folderHandle.child("Policies.json")
if (policiesFile.exists()) { if (policiesFile.exists()) {
policyBranches += createHashmap(jsonParser.getFromJson(Array<PolicyBranch>::class.java, policiesFile)) policyBranches += createHashmap(jsonParser.getFromJson(Array<PolicyBranch>::class.java, policiesFile))

View File

@ -282,4 +282,16 @@ fun String.equalsPlaceholderText(str:String): Boolean {
return this.getPlaceholderText() == str return this.getPlaceholderText() == str
} }
fun String.getPlaceholderParameters() = squareBraceRegex.findAll(this).map { it.groups[1]!!.value }.toList() fun String.getPlaceholderParameters() = squareBraceRegex.findAll(this).map { it.groups[1]!!.value }.toList()
/** Substitutes placeholders with [strings], respecting order of appearance. */
fun String.fillPlaceholders(vararg strings: String): String {
val keys = this.getPlaceholderParameters()
if (keys.size > strings.size)
throw Exception("String $this has a different number of placeholders ${keys.joinToString()} (${keys.size}) than the substitutive strings ${strings.joinToString()} (${strings.size})!")
var filledString = this
for (i in keys.indices)
filledString = filledString.replaceFirst(keys[i], strings[i])
return filledString
}

View File

@ -4,12 +4,10 @@ import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.SplitPane import com.badlogic.gdx.scenes.scene2d.ui.SplitPane
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.utils.Align
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.civilization.AlertType import com.unciv.logic.civilization.*
import com.unciv.logic.civilization.CityStateType
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.civilization.PopupAlert
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.diplomacy.DiplomacyManager import com.unciv.logic.civilization.diplomacy.DiplomacyManager
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers.* import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers.*
@ -18,8 +16,10 @@ import com.unciv.logic.trade.TradeLogic
import com.unciv.logic.trade.TradeOffer import com.unciv.logic.trade.TradeOffer
import com.unciv.logic.trade.TradeType import com.unciv.logic.trade.TradeType
import com.unciv.models.ruleset.ModOptionsConstants import com.unciv.models.ruleset.ModOptionsConstants
import com.unciv.models.ruleset.Quest
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.utils.* import com.unciv.ui.utils.*
import kotlin.math.floor
import kotlin.math.roundToInt import kotlin.math.roundToInt
import com.unciv.ui.utils.AutoScrollPane as ScrollPane import com.unciv.ui.utils.AutoScrollPane as ScrollPane
@ -65,6 +65,12 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() {
relationship.setSize(30f,30f) relationship.setSize(30f,30f)
civIndicator.addActor(relationship) civIndicator.addActor(relationship)
if (civ.isCityState() && civ.questManager.haveQuestsFor(viewingCiv)) {
val questIcon = ImageGetter.getImage("OtherIcons/Quest").surroundWithCircle(size = 30f, color = Color.GOLDENROD)
civIndicator.addActor(questIcon)
questIcon.setX(floor(civIndicator.width - questIcon.width))
}
leftSideTable.add(civIndicator).row() leftSideTable.add(civIndicator).row()
civIndicator.onClick { civIndicator.onClick {
@ -171,9 +177,34 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() {
} }
} }
for (assignedQuest in otherCiv.questManager.assignedQuests.filter { it.assignee == viewingCiv.civName}) {
diplomacyTable.addSeparator()
diplomacyTable.add(getQuestTable(assignedQuest)).row()
}
return diplomacyTable return diplomacyTable
} }
private fun getQuestTable(assignedQuest: AssignedQuest): Table {
val questTable = Table()
questTable.defaults().pad(10f)
val quest: Quest = viewingCiv.gameInfo.ruleSet.quests[assignedQuest.questName]!!
val remainingTurns: Int = assignedQuest.getRemainingTurns()
val title = "[${quest.name}] (+[${quest.influece.toInt()}] influence)"
val description = assignedQuest.getDescription()
questTable.add(title.toLabel(fontSize = 24)).row()
questTable.add(description.toLabel()).row()
if (quest.duration > 0)
questTable.add("Remaining [${remainingTurns}] turns".toLabel()).row()
questTable.onClick {
assignedQuest.onClickAction()
}
return questTable
}
private fun getMajorCivDiplomacyTable(otherCiv: CivilizationInfo): Table { private fun getMajorCivDiplomacyTable(otherCiv: CivilizationInfo): Table {
val otherCivDiplomacyManager = otherCiv.getDiplomacyManager(viewingCiv) val otherCivDiplomacyManager = otherCiv.getDiplomacyManager(viewingCiv)

View File

@ -166,8 +166,8 @@ fun Actor.onChange(function: () -> Unit): Actor {
return this return this
} }
fun Actor.surroundWithCircle(size:Float,resizeActor:Boolean=true): IconCircleGroup { fun Actor.surroundWithCircle(size: Float, resizeActor: Boolean = true, color: Color = Color.WHITE): IconCircleGroup {
return IconCircleGroup(size,this,resizeActor) return IconCircleGroup(size,this,resizeActor, color)
} }
fun Actor.addBorder(size:Float,color:Color,expandCell:Boolean=false):Table{ fun Actor.addBorder(size:Float,color:Color,expandCell:Boolean=false):Table{

View File

@ -1,10 +1,11 @@
package com.unciv.ui.utils package com.unciv.ui.utils
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Group import com.badlogic.gdx.scenes.scene2d.Group
class IconCircleGroup(size:Float, val actor: Actor, resizeActor:Boolean=true): Group(){ class IconCircleGroup(size: Float, val actor: Actor, resizeActor: Boolean = true, color: Color = Color.WHITE): Group(){
val circle = ImageGetter.getCircle().apply { setSize(size, size) } val circle = ImageGetter.getCircle().apply { setSize(size, size); setColor(color) }
init { init {
isTransform=false // performance helper - nothing here is rotated or scaled isTransform=false // performance helper - nothing here is rotated or scaled
setSize(size, size) setSize(size, size)