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 =
Ally =
[questName] (+[influenceAmount] influence) =
Remaining [remainingTurns] turns =
## Diplomatic modifiers
You declared war on us! =
@ -436,7 +439,8 @@ Our proposed trade request is no longer relevant! =
[building] has provided [amount] Gold! =
[civName] has stolen your territory! =
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
@ -916,3 +920,24 @@ in this city =
in every city =
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 getDifficulty() = difficultyObject
fun getCities() = civilizations.flatMap { it.cities }
fun getAliveCityStates() = civilizations.filter { it.isAlive() && it.isCityState() }
fun getAliveMajorCivs() = civilizations.filter { it.isAlive() && it.isMajorCiv() }
//endregion
fun nextTurn() {

View File

@ -3,7 +3,6 @@ package com.unciv.logic.civilization
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.JsonParser
import com.unciv.UncivGame
import com.unciv.logic.GameInfo
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.DiplomacyManager
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.TileInfo
import com.unciv.logic.trade.TradeEvaluation
import com.unciv.logic.trade.TradeRequest
import com.unciv.models.ruleset.*
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.stats.Stats
import com.unciv.models.translations.equalsPlaceholderText
@ -56,6 +57,7 @@ class CivilizationInfo {
var civName = ""
var tech = TechManager()
var policies = PolicyManager()
var questManager = QuestManager()
var goldenAges = GoldenAgeManager()
var greatPeople = GreatPersonManager()
var victoryManager=VictoryManager()
@ -89,6 +91,7 @@ class CivilizationInfo {
toReturn.civName = civName
toReturn.tech = tech.clone()
toReturn.policies = policies.clone()
toReturn.questManager = questManager.clone()
toReturn.goldenAges = goldenAges.clone()
toReturn.greatPeople = greatPeople.clone()
toReturn.victoryManager = victoryManager.clone()
@ -133,6 +136,8 @@ class CivilizationInfo {
fun isCityState(): Boolean = nation.isCityState()
fun getCityStateType(): CityStateType = nation.cityStateType!!
fun isMajorCiv() = nation.isMajorCiv()
fun isAlive(): Boolean = !isDefeated()
fun hasEverBeenFriendWith(otherCiv: CivilizationInfo): Boolean = getDiplomacyManager(otherCiv).everBeenFriends()
fun victoryType(): VictoryType {
if(gameInfo.gameParameters.victoryTypes.size==1)
@ -160,6 +165,8 @@ class CivilizationInfo {
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
@ -367,11 +374,15 @@ class CivilizationInfo {
fun setTransients() {
goldenAges.civInfo = this
policies.civInfo = this
if(policies.adoptedPolicies.size>0 && policies.numberOfAdoptedPolicies == 0)
policies.numberOfAdoptedPolicies = policies.adoptedPolicies.count { !it.endsWith("Complete") }
policies.setTransients()
questManager.civInfo = this
questManager.setTransients()
if(citiesCreated==0 && cities.any())
citiesCreated = cities.filter { it.name in nation.cities }.count()
@ -438,6 +449,9 @@ class CivilizationInfo {
policies.endTurn(nextTurnStats.culture.toInt())
if (isCityState())
questManager.endTurn()
// disband units until there are none left OR the gold values are normal
if (!isBarbarian() && gold < -100 && nextTurnStats.gold.toInt() < 0) {
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.unciv.ui.cityscreen.CityScreen
import com.unciv.ui.pickerscreens.TechPickerScreen
import com.unciv.ui.trade.DiplomacyScreen
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
}
fun getNumberOfTechsResearched(): Int = techsResearched.size
fun getRuleset() = civInfo.gameInfo.ruleSet
fun costOfTech(techName: String): Int {

View File

@ -33,7 +33,8 @@ enum class DiplomacyFlags{
SettledCitiesNearUs,
AgreedToNotSettleNearUs,
IgnoreThemSettlingNearUs,
ProvideMilitaryUnit
ProvideMilitaryUnit,
EverBeenFriends
}
enum class DiplomaticModifiers{
@ -285,6 +286,16 @@ class DiplomacyManager() {
nextTurnFlags()
if (civInfo.isCityState() && !otherCiv().isCityState())
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() {
@ -310,21 +321,35 @@ class DiplomacyManager() {
}
private fun nextTurnFlags() {
for (flag in flagsCountdown.keys.toList()) {
if (flag == DiplomacyFlags.ResearchAgreement.name){
loop@ for (flag in flagsCountdown.keys.toList()) {
// 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()
}
flagsCountdown[flag] = flagsCountdown[flag]!! - 1
// Only when flag is expired
if (flagsCountdown[flag] == 0) {
if (flag == DiplomacyFlags.ResearchAgreement.name && !otherCivDiplomacy().hasFlag(DiplomacyFlags.ResearchAgreement))
sciencefromResearchAgreement()
if (flag == DiplomacyFlags.ProvideMilitaryUnit.name && civInfo.cities.isEmpty() || otherCiv().cities.isEmpty())
continue
when (flag) {
DiplomacyFlags.ResearchAgreement.name -> {
if (!otherCivDiplomacy().hasFlag(DiplomacyFlags.ResearchAgreement))
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)
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
import com.badlogic.gdx.Files
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle
import com.unciv.Constants
import com.unciv.JsonParser
import com.unciv.UncivGame
import com.unciv.logic.UncivShowableException
import com.unciv.models.metadata.BaseRuleset
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.Promotion
import com.unciv.models.stats.INamed
import java.lang.StringBuilder
import kotlin.collections.set
object ModOptionsConstants {
@ -46,6 +42,7 @@ class Ruleset {
val units = LinkedHashMap<String, BaseUnit>()
val unitPromotions = LinkedHashMap<String, Promotion>()
val nations = LinkedHashMap<String, Nation>()
val quests = LinkedHashMap<String, Quest>()
val policyBranches = LinkedHashMap<String, PolicyBranch>()
val difficulties = LinkedHashMap<String, Difficulty>()
val mods = LinkedHashSet<String>()
@ -70,6 +67,7 @@ class Ruleset {
difficulties.putAll(ruleset.difficulties)
nations.putAll(ruleset.nations)
policyBranches.putAll(ruleset.policyBranches)
quests.putAll(ruleset.quests)
technologies.putAll(ruleset.technologies)
for (techToRemove in ruleset.modOptions.techsToRemove) technologies.remove(techToRemove)
terrains.putAll(ruleset.terrains)
@ -87,6 +85,7 @@ class Ruleset {
difficulties.clear()
nations.clear()
policyBranches.clear()
quests.clear()
technologies.clear()
buildings.clear()
terrains.clear()
@ -139,6 +138,9 @@ class Ruleset {
val promotionsFile = folderHandle.child("UnitPromotions.json")
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")
if (policiesFile.exists()) {
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
}
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.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.utils.Align
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.civilization.AlertType
import com.unciv.logic.civilization.CityStateType
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.civilization.PopupAlert
import com.unciv.logic.civilization.*
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.diplomacy.DiplomacyManager
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.TradeType
import com.unciv.models.ruleset.ModOptionsConstants
import com.unciv.models.ruleset.Quest
import com.unciv.models.translations.tr
import com.unciv.ui.utils.*
import kotlin.math.floor
import kotlin.math.roundToInt
import com.unciv.ui.utils.AutoScrollPane as ScrollPane
@ -65,6 +65,12 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() {
relationship.setSize(30f,30f)
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()
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
}
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 {
val otherCivDiplomacyManager = otherCiv.getDiplomacyManager(viewingCiv)

View File

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

View File

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