mirror of
https://github.com/yairm210/Unciv.git
synced 2025-01-03 13:30:51 +07:00
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:
parent
847abf31d1
commit
adaee7e7ab
BIN
android/Images/OtherIcons/Quest.png
Normal file
BIN
android/Images/OtherIcons/Quest.png
Normal file
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 |
113
android/assets/jsons/Civ V - Vanilla/Quests.json
Normal file
113
android/assets/jsons/Civ V - Vanilla/Quests.json
Normal 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": ""
|
||||
}
|
||||
*/
|
||||
]
|
@ -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. =
|
||||
|
@ -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() {
|
||||
|
@ -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)) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
378
core/src/com/unciv/logic/civilization/QuestManager.kt
Normal file
378
core/src/com/unciv/logic/civilization/QuestManager.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -59,6 +59,8 @@ class TechManager {
|
||||
return toReturn
|
||||
}
|
||||
|
||||
fun getNumberOfTechsResearched(): Int = techsResearched.size
|
||||
|
||||
fun getRuleset() = civInfo.gameInfo.ruleSet
|
||||
|
||||
fun costOfTech(techName: String): Int {
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
35
core/src/com/unciv/models/ruleset/Quest.kt
Normal file
35
core/src/com/unciv/models/ruleset/Quest.kt
Normal 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()
|
||||
}
|
@ -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))
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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{
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user