City-states personalities (#3259)

* City State Personality

* Introduced 4 personalities for city states: Friendly, Neutral, Hostile and Irrational.
* Influence recovery and degrade depends on city state personality.

* Quests assignement dependant on Personality and Trait

* Personality localization strings
This commit is contained in:
Federico Luongo 2020-10-14 08:51:31 +02:00 committed by GitHub
parent 8292b848b7
commit b5a32e64ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 162 additions and 32 deletions

View File

@ -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. =

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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()

View File

@ -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]

View File

@ -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 {

View File

@ -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()

View File

@ -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 <T> List<T>.randomWeighted(weights: List<Float>, 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()
}