diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 2ba8b5ec91..5cba131d36 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -138,6 +138,11 @@ Maritime = Mercantile = Militaristic = Type: = +Friendly = +Neutral = +Hostile = +Irrational = +Personality: = Influence: = Reach 30 for friendship. = Reach highest influence above 60 for alliance. = diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index 6c018a3b3d..971139982d 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -2,6 +2,7 @@ package com.unciv.logic import com.badlogic.gdx.math.Vector2 import com.unciv.Constants +import com.unciv.logic.civilization.CityStatePersonality import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.* import com.unciv.logic.map.mapgenerator.MapGenerator @@ -135,6 +136,7 @@ object GameStarter { for (cityStateName in availableCityStatesNames.take(newGameParameters.numberOfCityStates)) { val civ = CivilizationInfo(cityStateName) + civ.cityStatePersonality = CityStatePersonality.values().random() gameInfo.civilizations.add(civ) for(tech in ruleset.technologies.values.filter { it.uniques.contains("Starting tech") }) civ.tech.techsResearched.add(tech.name) // can't be .addTechnology because the civInfo isn't assigned yet diff --git a/core/src/com/unciv/logic/automation/NextTurnAutomation.kt b/core/src/com/unciv/logic/automation/NextTurnAutomation.kt index 1f8e0b7c56..d1c50e50d9 100644 --- a/core/src/com/unciv/logic/automation/NextTurnAutomation.kt +++ b/core/src/com/unciv/logic/automation/NextTurnAutomation.kt @@ -111,7 +111,7 @@ object NextTurnAutomation { private fun useGold(civInfo: CivilizationInfo) { if (civInfo.victoryType() == VictoryType.Cultural) { for (cityState in civInfo.getKnownCivs() - .filter { it.isCityState() && it.getCityStateType() == CityStateType.Cultured }) { + .filter { it.isCityState() && it.cityStateType == CityStateType.Cultured }) { val diploManager = cityState.getDiplomacyManager(civInfo) if (diploManager.influence < 40) { // we want to gain influence with them tryGainInfluence(civInfo, cityState) @@ -122,7 +122,7 @@ object NextTurnAutomation { if (civInfo.getHappiness() < 5) { for (cityState in civInfo.getKnownCivs() - .filter { it.isCityState() && it.getCityStateType() == CityStateType.Mercantile }) { + .filter { it.isCityState() && it.cityStateType == CityStateType.Mercantile }) { val diploManager = cityState.getDiplomacyManager(civInfo) if (diploManager.influence < 40) { // we want to gain influence with them tryGainInfluence(civInfo, cityState) diff --git a/core/src/com/unciv/logic/city/CityStats.kt b/core/src/com/unciv/logic/city/CityStats.kt index b54812f975..8d0369db35 100644 --- a/core/src/com/unciv/logic/city/CityStats.kt +++ b/core/src/com/unciv/logic/city/CityStats.kt @@ -126,7 +126,7 @@ class CityStats { val stats = Stats() for (otherCiv in cityInfo.civInfo.getKnownCivs()) { - if (otherCiv.isCityState() && otherCiv.getCityStateType() == CityStateType.Maritime + if (otherCiv.isCityState() && otherCiv.cityStateType == CityStateType.Maritime && otherCiv.getDiplomacyManager(cityInfo.civInfo).relationshipLevel() >= RelationshipLevel.Friend) { if (cityInfo.isCapital()) stats.food += 3 else stats.food += 1 diff --git a/core/src/com/unciv/logic/civilization/CityStateType.kt b/core/src/com/unciv/logic/civilization/CityStateType.kt index 9f9dce2860..d5169de74a 100644 --- a/core/src/com/unciv/logic/civilization/CityStateType.kt +++ b/core/src/com/unciv/logic/civilization/CityStateType.kt @@ -1,8 +1,15 @@ package com.unciv.logic.civilization -enum class CityStateType{ +enum class CityStateType { Cultured, Maritime, Mercantile, Militaristic +} + +enum class CityStatePersonality { + Friendly, + Neutral, + Hostile, + Irrational } \ No newline at end of file diff --git a/core/src/com/unciv/logic/civilization/CivInfoStats.kt b/core/src/com/unciv/logic/civilization/CivInfoStats.kt index da9d728caf..5d18a7dfa3 100644 --- a/core/src/com/unciv/logic/civilization/CivInfoStats.kt +++ b/core/src/com/unciv/logic/civilization/CivInfoStats.kt @@ -85,7 +85,7 @@ class CivInfoStats(val civInfo: CivilizationInfo){ //City-States culture bonus for (otherCiv in civInfo.getKnownCivs()) { - if (otherCiv.isCityState() && otherCiv.getCityStateType() == CityStateType.Cultured + if (otherCiv.isCityState() && otherCiv.cityStateType == CityStateType.Cultured && otherCiv.getDiplomacyManager(civInfo.civName).relationshipLevel() >= RelationshipLevel.Friend) { val cultureBonus = Stats() var culture = 3f * (civInfo.getEraNumber() + 1) @@ -153,7 +153,7 @@ class CivInfoStats(val civInfo: CivilizationInfo){ //From city-states for (otherCiv in civInfo.getKnownCivs()) { - if (otherCiv.isCityState() && otherCiv.getCityStateType() == CityStateType.Mercantile + if (otherCiv.isCityState() && otherCiv.cityStateType == CityStateType.Mercantile && otherCiv.getDiplomacyManager(civInfo).relationshipLevel() >= RelationshipLevel.Friend) { if (statMap.containsKey("City-States")) statMap["City-States"] = statMap["City-States"]!! + 3f diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index 4f7cdc005f..d375b5ec96 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -109,6 +109,7 @@ class CivilizationInfo { toReturn.popupAlerts.addAll(popupAlerts) toReturn.tradeRequests.addAll(tradeRequests) toReturn.naturalWonders.addAll(naturalWonders) + toReturn.cityStatePersonality = cityStatePersonality return toReturn } @@ -117,7 +118,6 @@ class CivilizationInfo { if (isPlayerCivilization()) return gameInfo.getDifficulty() return gameInfo.ruleSet.difficulties["Chieftain"]!! } - fun getDiplomacyManager(civInfo: CivilizationInfo) = getDiplomacyManager(civInfo.civName) fun getDiplomacyManager(civName: String) = diplomacy[civName]!! /** Returns only undefeated civs, aka the ones we care about */ @@ -134,7 +134,8 @@ class CivilizationInfo { fun isBarbarian() = nation.isBarbarian() fun isSpectator() = nation.isSpectator() fun isCityState(): Boolean = nation.isCityState() - fun getCityStateType(): CityStateType = nation.cityStateType!! + val cityStateType: CityStateType get() = nation.cityStateType!! + var cityStatePersonality: CityStatePersonality = CityStatePersonality.Neutral fun isMajorCiv() = nation.isMajorCiv() fun isAlive(): Boolean = !isDefeated() fun hasEverBeenFriendWith(otherCiv: CivilizationInfo): Boolean = getDiplomacyManager(otherCiv).everBeenFriends() diff --git a/core/src/com/unciv/logic/civilization/QuestManager.kt b/core/src/com/unciv/logic/civilization/QuestManager.kt index 7f3abfeb83..fede1efec8 100644 --- a/core/src/com/unciv/logic/civilization/QuestManager.kt +++ b/core/src/com/unciv/logic/civilization/QuestManager.kt @@ -14,6 +14,7 @@ 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 com.unciv.ui.utils.randomWeighted import kotlin.math.max import kotlin.random.Random @@ -150,10 +151,10 @@ class QuestManager { if (numberValidMajorCivs >= quest.minimumCivs) assignableQuests.add(quest) } + val weights = assignableQuests.map { getQuestWeight(it.name) } - //TODO: quest probabilities should change based on City State personality and traits if (assignableQuests.isNotEmpty()) { - val quest = assignableQuests.random() + val quest = assignableQuests.randomWeighted(weights) val assignees = civInfo.gameInfo.getAliveMajorCivs().filter { !it.isAtWarWith(civInfo) && isQuestValid(quest, it) } assignNewQuest(quest, assignees) @@ -172,13 +173,14 @@ class QuestManager { 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 weights = assignableQuests.map { getQuestWeight(it.name) } - val quest = assignableQuests.random() + if (assignableQuests.isNotEmpty()) { + val quest = assignableQuests.randomWeighted(weights) val assignees = arrayListOf(challenger) assignNewQuest(quest, assignees) + break } } } @@ -365,6 +367,67 @@ class QuestManager { assignedQuests.removeAll(matchingQuests) } + /** + * Returns the weight of the [questName], depends on city state trait and personality + */ + private fun getQuestWeight(questName: String): Float { + var weight = 1f + val trait = civInfo.cityStateType + val personality = civInfo.cityStatePersonality + when (questName) { + QuestName.Route.value -> { + when (personality) { + CityStatePersonality.Friendly -> weight *= 2f + CityStatePersonality.Hostile -> weight *= .2f + } + when (trait) { + CityStateType.Maritime -> weight *= 1.2f + CityStateType.Mercantile -> weight *= 1.5f + } + } + QuestName.ConnectResource.value -> { + when (trait) { + CityStateType.Maritime -> weight *= 2f + CityStateType.Mercantile -> weight *= 3f + } + } + QuestName.ConstructWonder.value -> { + if (trait == CityStateType.Cultured) + weight *= 3f + } + QuestName.GreatPerson.value -> { + if (trait == CityStateType.Cultured) + weight *= 3f + } + QuestName.ConquerCityState.value -> { + if (trait == CityStateType.Militaristic) + weight *= 2f + when (personality) { + CityStatePersonality.Hostile -> weight *= 2f + CityStatePersonality.Neutral -> weight *= .4f + } + } + QuestName.FindPlayer.value -> { + when (trait) { + CityStateType.Maritime -> weight *= 3f + CityStateType.Mercantile -> weight *= 2f + } + } + QuestName.FindNaturalWonder.value -> { + if (trait == CityStateType.Militaristic) + weight *= .5f + if (personality == CityStatePersonality.Hostile) + weight *= .3f + } + QuestName.ClearBarbarianCamp.value -> { + weight *= 3f + if (trait == CityStateType.Militaristic) + weight *= 3f + } + } + return weight + } + //region get-quest-target /** * Returns a random [TileInfo] containing a Barbarian encampment within 8 tiles of [civInfo] diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt index d95a0609da..c418abe175 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt @@ -166,20 +166,55 @@ class DiplomacyManager() { return otherCivDiplomacy().getTurnsToRelationshipChange() if (civInfo.isCityState() && !otherCiv().isCityState()) { - val dropPerTurn = getCityStateInfluenceDegradeRate() - when { - relationshipLevel() >= RelationshipLevel.Ally -> return ceil((influence - 60f) / dropPerTurn).toInt() + 1 - relationshipLevel() >= RelationshipLevel.Friend -> return ceil((influence - 30f) / dropPerTurn).toInt() + 1 - else -> return 0 + val dropPerTurn = getCityStateInfluenceDegrade() + return when { + relationshipLevel() >= RelationshipLevel.Ally -> ceil((influence - 60f) / dropPerTurn).toInt() + 1 + relationshipLevel() >= RelationshipLevel.Friend -> ceil((influence - 30f) / dropPerTurn).toInt() + 1 + else -> 0 } } return 0 } - fun getCityStateInfluenceDegradeRate(): Float { - if(otherCiv().hasUnique("City-State Influence degrades at half rate")) - return .5f - else return 1f + private fun getCityStateInfluenceDegrade(): Float { + if (influence < restingPoint) + return 0f + + var decrement = when (civInfo.cityStatePersonality) { + CityStatePersonality.Hostile -> 1.5f + else -> 1f + } + + var modifier = when (civInfo.cityStatePersonality) { + CityStatePersonality.Hostile -> 2f + CityStatePersonality.Irrational -> 1.5f + CityStatePersonality.Friendly -> .5f + else -> 1f + } + + if (otherCiv().hasUnique("City-State Influence degrades at half rate")) + modifier *= .5f + + return max(0f, decrement) * max(0f, modifier) + } + + private fun getCityStateInfluenceRecovery(): Float { + if (influence > restingPoint) + return 0f + + var increment = 1f + + var modifier = when (civInfo.cityStatePersonality) { + CityStatePersonality.Friendly -> 2f + CityStatePersonality.Irrational -> 1.5f + CityStatePersonality.Hostile -> .5f + else -> 1f + } + + if (otherCiv().hasUnique("City-State Influence recovers at twice the normal rate")) + modifier *= 2f + + return max(0f, increment) * max(0f, modifier) } fun canDeclareWar() = turnsToPeaceTreaty()==0 && diplomaticStatus != DiplomaticStatus.War @@ -300,14 +335,13 @@ class DiplomacyManager() { private fun nextTurnCityStateInfluence() { val initialRelationshipLevel = relationshipLevel() - val increment = if (otherCiv().hasUnique("City-State Influence recovers at twice the normal rate")) 2f else 1f - val decrement = getCityStateInfluenceDegradeRate() - - if (influence > restingPoint) + if (influence > restingPoint) { + val decrement = getCityStateInfluenceDegrade() influence = max(restingPoint, influence - decrement) - else if (influence < restingPoint) + } else if (influence < restingPoint) { + val increment = getCityStateInfluenceRecovery() influence = min(restingPoint, influence + increment) - else influence = restingPoint + } if(!civInfo.isDefeated()) { // don't display city state relationship notifications when the city state is currently defeated val civCapitalLocation = if (civInfo.cities.isNotEmpty()) civInfo.getCapital().location else null @@ -400,7 +434,7 @@ class DiplomacyManager() { if (!hasFlag(DiplomacyFlags.DeclarationOfFriendship)) revertToZero(DiplomaticModifiers.DeclarationOfFriendship, 1 / 2f) //decreases slowly and will revert to full if it is declared later - if (otherCiv().isCityState() && otherCiv().getCityStateType() == CityStateType.Militaristic) { + if (otherCiv().isCityState() && otherCiv().cityStateType == CityStateType.Militaristic) { if (relationshipLevel() < RelationshipLevel.Friend) { if (hasFlag(DiplomacyFlags.ProvideMilitaryUnit)) removeFlag(DiplomacyFlags.ProvideMilitaryUnit) } else { diff --git a/core/src/com/unciv/ui/trade/DiplomacyScreen.kt b/core/src/com/unciv/ui/trade/DiplomacyScreen.kt index 9d84ef6540..f7cce17c9d 100644 --- a/core/src/com/unciv/ui/trade/DiplomacyScreen.kt +++ b/core/src/com/unciv/ui/trade/DiplomacyScreen.kt @@ -94,14 +94,14 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() { } - private fun getCityStateDiplomacyTable(otherCiv: CivilizationInfo): Table { val otherCivDiplomacyManager = otherCiv.getDiplomacyManager(viewingCiv) val diplomacyTable = Table() diplomacyTable.defaults().pad(10f) diplomacyTable.add(otherCiv.getLeaderDisplayName().toLabel(fontSize = 24)).row() - diplomacyTable.add(("Type: ".tr() + otherCiv.getCityStateType().toString().tr()).toLabel()).row() + diplomacyTable.add("{Type: } {${otherCiv.cityStateType}}".toLabel()).row() + diplomacyTable.add("{Personality: } {${otherCiv.cityStatePersonality}}".toLabel()).row() otherCiv.updateAllyCivForCityState() val ally = otherCiv.getAllyCiv() if (ally != "") { @@ -120,7 +120,7 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() { diplomacyTable.add(nextLevelString.toLabel()).row() } - val friendBonusText = when (otherCiv.getCityStateType()) { + val friendBonusText = when (otherCiv.cityStateType) { CityStateType.Cultured -> ("Provides [" + (3 * (viewingCiv.getEraNumber() + 1)).toString() + "] culture at 30 Influence").tr() CityStateType.Maritime -> "Provides 3 food in capital and 1 food in other cities at 30 Influence".tr() CityStateType.Mercantile -> "Provides 3 happiness at 30 Influence".tr() diff --git a/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt b/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt index 99872a1cc0..40ba5e762a 100644 --- a/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt +++ b/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt @@ -21,6 +21,7 @@ import com.unciv.models.UncivSound import com.unciv.models.translations.tr import com.unciv.ui.tutorials.TutorialController import kotlin.concurrent.thread +import kotlin.random.Random open class CameraStageBaseScreen : Screen { @@ -267,4 +268,21 @@ fun Label.setFontSize(size:Int): Label { style.font = Fonts.font style = style // because we need it to call the SetStyle function. Yuk, I know. return this.apply { setFontScale(size/ORIGINAL_FONT_SIZE) } // for chaining +} + + +fun List.randomWeighted(weights: List, random: Random = Random): T { + if (this.isEmpty()) throw NoSuchElementException("Empty list.") + if (this.size != weights.size) throw UnsupportedOperationException("Weights size does not match this list size.") + + val totalWeight = weights.sum() + val randDouble = random.nextDouble() + var sum = 0f + + for (i in weights.indices) { + sum += weights[i] / totalWeight + if (randDouble <= sum) + return this[i] + } + return this.last() } \ No newline at end of file