From 253e62de72a8999d78c7d49c9663e091238df355 Mon Sep 17 00:00:00 2001 From: Xander Lenstra <71121390+xlenstra@users.noreply.github.com> Date: Sat, 10 Jul 2021 20:51:11 +0200 Subject: [PATCH] City states give gold when met; updates to city state gold gifts (#4435) * City States now give gold when met * City States can now receive different amounts of gold, and the amount of influence gained from gifts follows the base game * Implemented requested changes * Fixed tests --- .../jsons/translations/template.properties | 3 + .../logic/city/CityInfoConquestFunctions.kt | 4 +- .../civilization/CivInfoTransientUpdater.kt | 2 +- .../logic/civilization/CivilizationInfo.kt | 70 ++++++++++++++----- core/src/com/unciv/logic/trade/TradeLogic.kt | 2 +- .../src/com/unciv/ui/trade/DiplomacyScreen.kt | 54 ++++++++++---- .../unciv/ui/worldscreen/unit/UnitActions.kt | 2 +- .../diplomacy/DiplomacyManagerTests.kt | 6 +- 8 files changed, 108 insertions(+), 35 deletions(-) diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 2cfbf46d17..bb7f920fd9 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -137,6 +137,7 @@ Provides [amountOfCulture] culture at 30 Influence = Provides 3 food in capital and 1 food in other cities at 30 Influence = Provides 3 happiness at 30 Influence = Provides land units every 20 turns at 30 Influence = +Give a Gift = Gift [giftAmount] gold (+[influenceAmount] influence) = Relationship changes in another [turnsToRelationshipChange] turns = Protected by = @@ -407,6 +408,8 @@ You have entered a Golden Age! = [n] sources of [resourceName] revealed, e.g. near [cityName] = A [greatPerson] has been born in [cityName]! = We have encountered [civName]! = +[cityStateName] has given us [stats] as a token of goodwill for meeting us = +[cityStateName] has given us [stats] as we are the first major civ to meet them = Cannot provide unit upkeep for [unitName] - unit has been disbanded! = [cityName] has grown! = [cityName] is starving! = diff --git a/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt b/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt index c7e92312c2..def6751248 100644 --- a/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt +++ b/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt @@ -84,7 +84,7 @@ class CityInfoConquestFunctions(val city: CityInfo){ // How can you conquer a city but not know the civ you conquered it from?! // I don't know either, but some of our players have managed this, and crashed their game! if (!conqueringCiv.knows(oldCiv)) - conqueringCiv.meetCivilization(oldCiv) + conqueringCiv.makeCivilizationsMeet(oldCiv) oldCiv.getDiplomacyManager(conqueringCiv) .addModifier(DiplomaticModifiers.CapturedOurCities, -aggroGenerated) @@ -145,7 +145,7 @@ class CityInfoConquestFunctions(val city: CityInfo){ // In order to get "plus points" in Diplomacy, you have to establish diplomatic relations if you haven't yet if (!conqueringCiv.knows(foundingCiv)) - conqueringCiv.meetCivilization(foundingCiv) + conqueringCiv.makeCivilizationsMeet(foundingCiv) if (foundingCiv.isMajorCiv()) { foundingCiv.getDiplomacyManager(conqueringCiv) diff --git a/core/src/com/unciv/logic/civilization/CivInfoTransientUpdater.kt b/core/src/com/unciv/logic/civilization/CivInfoTransientUpdater.kt index e7efad99dd..3168d658a9 100644 --- a/core/src/com/unciv/logic/civilization/CivInfoTransientUpdater.kt +++ b/core/src/com/unciv/logic/civilization/CivInfoTransientUpdater.kt @@ -38,7 +38,7 @@ class CivInfoTransientUpdater(val civInfo: CivilizationInfo) { for (entry in viewedCivs) { val metCiv = entry.key if (metCiv == civInfo || metCiv.isBarbarian() || civInfo.diplomacy.containsKey(metCiv.civName)) continue - civInfo.meetCivilization(metCiv) + civInfo.makeCivilizationsMeet(metCiv) civInfo.addNotification("We have encountered [" + metCiv.civName + "]!", entry.value.position, metCiv.civName, NotificationIcon.Diplomacy) metCiv.addNotification("We have encountered [" + civInfo.civName + "]!", entry.value.position, civInfo.civName, NotificationIcon.Diplomacy) } diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index b1829b9cf6..1e4602d817 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -29,6 +29,7 @@ import kotlin.collections.ArrayList import kotlin.collections.HashMap import kotlin.math.roundToInt import kotlin.math.min +import kotlin.math.pow class CivilizationInfo { @@ -328,18 +329,40 @@ class CivilizationInfo { return baseUnit } - fun meetCivilization(otherCiv: CivilizationInfo) { + fun makeCivilizationsMeet(otherCiv: CivilizationInfo) { + meetCiv(otherCiv) + otherCiv.meetCiv(this) + } + + private fun meetCiv(otherCiv: CivilizationInfo) { diplomacy[otherCiv.civName] = DiplomacyManager(this, otherCiv.civName) - .apply { diplomaticStatus = DiplomaticStatus.Peace } + .apply { diplomaticStatus = DiplomaticStatus.Peace } otherCiv.popupAlerts.add(PopupAlert(AlertType.FirstContact, civName)) - otherCiv.diplomacy[civName] = DiplomacyManager(otherCiv, civName) - .apply { diplomaticStatus = DiplomaticStatus.Peace } - popupAlerts.add(PopupAlert(AlertType.FirstContact, otherCiv.civName)) - - if (isCurrentPlayer() || otherCiv.isCurrentPlayer()) + if (isCurrentPlayer()) UncivGame.Current.settings.addCompletedTutorialTask("Meet another civilization") + + if (!(isCityState() && otherCiv.isMajorCiv())) return + + val cityStateLocation = if (cities.isEmpty()) null else getCapital().location + + val giftAmount = Stats().add(Stat.Gold, 15f) + // Later, religious city-states will also gift gold, making this the better implementation + // For now, it might be overkill though. + var meetString = "[${civName}] has given us [${giftAmount}] as a token of goodwill for meeting us" + if (diplomacy.filter { it.value.otherCiv().isMajorCiv() }.count() == 1) { + giftAmount.timesInPlace(2f) + meetString = "[${civName}] has given us [${giftAmount}] as we are the first major civ to meet them" + } + if (cityStateLocation != null) + otherCiv.addNotification(meetString, cityStateLocation, NotificationIcon.Gold) + else + otherCiv.addNotification(meetString, NotificationIcon.Gold) + for (stat in giftAmount.toHashMap().filter { it.value != 0f }) + otherCiv.addStat(stat.key, stat.value.toInt()) + + } fun discoverNaturalWonder(naturalWonderName: String) { @@ -699,11 +722,32 @@ class CivilizationInfo { diplomacyManager.otherCiv().tradeRequests.remove(tradeRequest) // it would be really weird to get a trade request from a dead civ } } + + fun getResearchAgreementCost(): Int { + // https://forums.civfanatics.com/resources/research-agreements-bnw.25568/ + val era = if (getEra() in gameInfo.ruleSet.eras) gameInfo.ruleSet.eras[getEra()]!! else Era() + return (era.researchAgreementCost * gameInfo.gameParameters.gameSpeed.modifier).toInt() + } + + //////////////////////// Functions specific to City State civilizations //////////////////////// - fun influenceGainedByGift(cityState: CivilizationInfo, giftAmount: Int): Int { - var influenceGained = giftAmount / 10f + + fun influenceGainedByGift(giftAmount: Int): Int { + // https://github.com/Gedemon/Civ5-DLL/blob/aa29e80751f541ae04858b6d2a2c7dcca454201e/CvGameCoreDLL_Expansion1/CvMinorCivAI.cpp + // line 8681 and below + var influenceGained = giftAmount.toFloat().pow(1.01f) / 9.8f + val gameProgressApproximate = min(gameInfo.turns / (400f * gameInfo.gameParameters.gameSpeed.modifier), 1f) + influenceGained *= 1 - (2/3) * gameProgressApproximate + influenceGained *= when (gameInfo.gameParameters.gameSpeed) { + GameSpeed.Quick -> 1.25f + GameSpeed.Standard -> 1f + GameSpeed.Epic -> 0.75f + GameSpeed.Marathon -> 0.67f + } for (unique in getMatchingUniques("Gifts of Gold to City-States generate []% more Influence")) influenceGained *= 1f + unique.params[0].toFloat() / 100f + influenceGained -= influenceGained % 5 + if (influenceGained < 5f) influenceGained = 5f return influenceGained.toInt() } @@ -711,17 +755,11 @@ class CivilizationInfo { if (!cityState.isCityState()) throw Exception("You can only gain influence with City-States!") addGold(-giftAmount) cityState.addGold(giftAmount) - cityState.getDiplomacyManager(this).influence += influenceGainedByGift(cityState, giftAmount) + cityState.getDiplomacyManager(this).influence += influenceGainedByGift(giftAmount) cityState.updateAllyCivForCityState() updateStatsForNextTurn() } - fun getResearchAgreementCost(): Int { - // https://forums.civfanatics.com/resources/research-agreements-bnw.25568/ - val era = if (getEra() in gameInfo.ruleSet.eras) gameInfo.ruleSet.eras[getEra()]!! else Era() - return (era.researchAgreementCost * gameInfo.gameParameters.gameSpeed.modifier).toInt() - } - fun gainMilitaryUnitFromCityState(otherCiv: CivilizationInfo) { val cities = NextTurnAutomation.getClosestCities(this, otherCiv) val city = cities.city1 diff --git a/core/src/com/unciv/logic/trade/TradeLogic.kt b/core/src/com/unciv/logic/trade/TradeLogic.kt index 2327eb7559..b71ad1cfdb 100644 --- a/core/src/com/unciv/logic/trade/TradeLogic.kt +++ b/core/src/com/unciv/logic/trade/TradeLogic.kt @@ -107,7 +107,7 @@ class TradeLogic(val ourCivilization:CivilizationInfo, val otherCivilization: Ci } } if (offer.type == TradeType.Introduction) - to.meetCivilization(to.gameInfo.getCivilization(offer.name)) + to.makeCivilizationsMeet(to.gameInfo.getCivilization(offer.name)) if (offer.type == TradeType.WarDeclaration) { val nameOfCivToDeclareWarOn = offer.name diff --git a/core/src/com/unciv/ui/trade/DiplomacyScreen.kt b/core/src/com/unciv/ui/trade/DiplomacyScreen.kt index 2bc0cadd76..47166bfa26 100644 --- a/core/src/com/unciv/ui/trade/DiplomacyScreen.kt +++ b/core/src/com/unciv/ui/trade/DiplomacyScreen.kt @@ -29,7 +29,7 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() { private val leftSideTable = Table().apply { defaults().pad(10f) } private val rightSideTable = Table() - + private fun isNotPlayersTurn() = !UncivGame.Current.worldScreen.isPlayersTurn init { @@ -92,8 +92,7 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() { return tradeTable } - - private fun getCityStateDiplomacyTable(otherCiv: CivilizationInfo): Table { + private fun getCityStateDiplomacyTableHeader(otherCiv: CivilizationInfo): Table { val otherCivDiplomacyManager = otherCiv.getDiplomacyManager(viewingCiv) val diplomacyTable = Table() @@ -144,21 +143,26 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() { friendBonusLabelColor = Color.GRAY val friendBonusLabel = friendBonusText.toLabel(friendBonusLabelColor) - .apply { setAlignment(Align.center) } + .apply { setAlignment(Align.center) } diplomacyTable.add(friendBonusLabel).row() + + return diplomacyTable + } + + private fun getCityStateDiplomacyTable(otherCiv: CivilizationInfo): Table { + val otherCivDiplomacyManager = otherCiv.getDiplomacyManager(viewingCiv) + + val diplomacyTable = getCityStateDiplomacyTableHeader(otherCiv) diplomacyTable.addSeparator() - val giftAmount = 250 - val influenceAmount = viewingCiv.influenceGainedByGift(otherCiv, giftAmount) - val giftButton = "Gift [$giftAmount] gold (+[$influenceAmount] influence)".toTextButton() - giftButton.onClick { - viewingCiv.giveGoldGift(otherCiv, giftAmount) - updateRightSide(otherCiv) + val giveGoldButton = "Give a Gift".toTextButton() + giveGoldButton.onClick { + rightSideTable.clear() + rightSideTable.add(ScrollPane(getGoldGiftTable(otherCiv))) } - diplomacyTable.add(giftButton).row() - if (viewingCiv.gold < giftAmount || isNotPlayersTurn()) giftButton.disable() - + diplomacyTable.add(giveGoldButton).row() + if (otherCivDiplomacyManager.diplomaticStatus == DiplomaticStatus.Protector){ val revokeProtectionButton = "Revoke Protection".toTextButton() revokeProtectionButton.onClick { @@ -216,6 +220,30 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() { return diplomacyTable } + + private fun getGoldGiftTable(otherCiv: CivilizationInfo): Table { + val diplomacyTable = getCityStateDiplomacyTableHeader(otherCiv) + diplomacyTable.addSeparator() + + for (giftAmount in listOf(250, 500, 1000)) { + val influenceAmount = viewingCiv.influenceGainedByGift(giftAmount) + val giftButton = "Gift [$giftAmount] gold (+[$influenceAmount] influence)".toTextButton() + giftButton.onClick { + viewingCiv.giveGoldGift(otherCiv, giftAmount) + updateRightSide(otherCiv) + } + diplomacyTable.add(giftButton).row() + if (viewingCiv.gold < giftAmount || isNotPlayersTurn()) giftButton.disable() + } + + val backButton = "Back".toTextButton() + backButton.onClick { + rightSideTable.clear() + rightSideTable.add(ScrollPane(getCityStateDiplomacyTable(otherCiv))) + } + diplomacyTable.add(backButton) + return diplomacyTable + } private fun getQuestTable(assignedQuest: AssignedQuest): Table { val questTable = Table() diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt index 104c9dd83a..688a6def13 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt @@ -518,7 +518,7 @@ object UnitActions { val otherCiv = tile.getOwner() if (otherCiv != null) { // decrease relations for -10 pt/tile - if (!otherCiv.knows(unit.civInfo)) otherCiv.meetCivilization(unit.civInfo) + if (!otherCiv.knows(unit.civInfo)) otherCiv.makeCivilizationsMeet(unit.civInfo) otherCiv.getDiplomacyManager(unit.civInfo).addModifier(DiplomaticModifiers.StealingTerritory, -10f) civsToNotify.add(otherCiv) } diff --git a/tests/src/com/unciv/logic/civilization/diplomacy/DiplomacyManagerTests.kt b/tests/src/com/unciv/logic/civilization/diplomacy/DiplomacyManagerTests.kt index 5933fa7e45..99c23be64b 100644 --- a/tests/src/com/unciv/logic/civilization/diplomacy/DiplomacyManagerTests.kt +++ b/tests/src/com/unciv/logic/civilization/diplomacy/DiplomacyManagerTests.kt @@ -2,6 +2,7 @@ package com.unciv.logic.civilization.diplomacy import com.unciv.logic.GameInfo import com.unciv.logic.civilization.CivilizationInfo +import com.unciv.models.ruleset.Nation import com.unciv.testing.GdxTestRunner import io.mockk.every import io.mockk.mockk @@ -23,11 +24,14 @@ class DiplomacyManagerTests { private fun meetByName(civilization: String, civilizationToMeet: String) { civilizations.getValue(civilization) - .meetCivilization(civilizations.getValue(civilizationToMeet)) + .makeCivilizationsMeet(civilizations.getValue(civilizationToMeet)) } @Before fun setup() { + // Add nations to test civilizations, as we need them to know that they are major civs + civilizations.values.forEach { it.nation = Nation() } + // Setup the GameInfo mock every { mockGameInfo.getCivilization(capture(slot)) } answers { civilizations.getValue(slot.captured) }