Created Patronage policy branch (#4186)

* Created Patronage policy branch -- draft

* Patronage branch is now functional

* Added images for the policies

* Temporarily bandaged backwards compatability, added incompatabilities

* Implemented recommended changes

* Fixed acquirement of 'patronage complete' not being saved

* Reverted change I was unhappy with

* Implemented requested changes

* Fixed build errors

* Implemented recommended changes

* City States can now give any great person, including unique ones, conform Ravignirs tests
This commit is contained in:
Xander Lenstra 2021-06-22 16:25:29 +02:00 committed by GitHub
parent a025660fe0
commit 7f88844d82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 579 additions and 385 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 988 KiB

After

Width:  |  Height:  |  Size: 998 KiB

View File

@ -135,7 +135,7 @@
},{
"name": "Piety",
"era": "Classical era",
"uniques": ["+[15]% Production when constructing [Culture] buildings"],
"uniques": ["+[15]% Production when constructing [Culture] buildings", "Incompatible with [Rationalism]"],
"policies": [
{
"name": "Organized Religion",
@ -177,8 +177,15 @@
"uniques": ["Culture cost of adopting new Policies reduced by [10]%"]
}
]
},/*{
"name": "Patronage",
},
{
"name": "Patronage ",
// Yes, there is a space behind this word, and yes, this is necessary
// This is, because at the time of writing there existed another policy called 'Patronage' that was recently deprecated
// It would, however, still be possible for save-files to contain this policy
// Therefore, we had to differentiate between these two, and this was the least intrusive way to do so
// NOTE: If you remove the space here, also remove the extra space between 'patronage' and 'complete' in the 'patronage complete' policy.
// Otherwise, weird stuff will happen.
"era": "Classical era",
"uniques": ["City-State Influence degrades [25]% slower"],
"policies": [
@ -196,14 +203,15 @@
},
{
"name": "Scholasticism",
"uniques":["Allied City-States provide Science equal to [25]% of what they produce for themselves"],
"uniques":["Allied City-States provide [Science] equal to [25]% of what they produce for themselves"],
"requires": ["Philantropy"],
"row": 2,
"column": 2
},
{
"name": "Cultural Diplomacy",
"uniques": ["Quantity of Resources gifted by City-States increased by [100]%"],
"uniques": ["Quantity of Resources gifted by City-States increased by [100]%",
"Happiness from Luxury Resources gifted by City-States increased by [50]%"],
"requires": ["Scholasticism"],
"row": 3,
"column": 2
@ -211,14 +219,19 @@
{
"name": "Educated Elite",
"requires": ["Scholasticism","Aesthetics"],
"uniques": ["Allied City-States will occasionally gift Great People"],
"row": 3,
"column": 4
},
{
"name": "Patronage Complete"
}
"name": "Patronage Complete",
// This extra space is intentional, see above.
"uniques": ["Influence of all other civilizations with all city-states degrades [33]% faster",
"Triggers the following global alert: [Our influence with City-States has started dropping faster!]"]
// This triggers a global alert in the G&K game also, based on my memory of playing it
}
]
},*/
},
{
"name": "Commerce",
"uniques": ["+[25]% [Gold] [in capital]"],
@ -269,7 +282,7 @@
{
"name": "Rationalism",
"era": "Renaissance era",
"uniques": ["+[15]% [Science] while the empire is happy"],
"uniques": ["+[15]% [Science] while the empire is happy", "Incompatible with [Piety]"],
"policies": [
{
"name": "Secularism",

View File

@ -483,6 +483,9 @@ Clearing a [forest] has created [amount] Production for [cityName] =
The resistance in [cityName] has ended! =
Our [name] took [tileDamage] tile damage and was destroyed =
Our [name] took [tileDamage] tile damage =
[civName] has adopted the [policyName] policy =
An unknown civilization has adopted the [policyName] policy =
Our influence with City-States has started dropping faster! =
# World Screen UI

View File

@ -103,13 +103,26 @@ class CivInfoStats(val civInfo: CivilizationInfo) {
statMap.add("City-States", cultureBonus)
}
if (otherCiv.isCityState() && otherCiv.getDiplomacyManager(civInfo.civName).relationshipLevel() >= RelationshipLevel.Ally) {
val sciencePercentage = civInfo
.getMatchingUniques("Allied City-States provide Science equal to []% of what they produce for themselves")
.sumBy { it.params[0].toInt() }
statMap.add("City-States", Stats().apply { science = otherCiv.statsForNextTurn.science * (sciencePercentage / 100f) })
}
if (otherCiv.isCityState())
for (unique in civInfo.getMatchingUniques("Allied City-States provide [] equal to []% of what they produce for themselves")) {
if (otherCiv.diplomacy[civInfo.civName]!!.matchesCityStateRelationshipFilter(unique.params[0]) && otherCiv.cities.isNotEmpty()) {
statMap.add(
"City-States",
Stats().add(
Stat.valueOf(unique.params[1]),
otherCiv.statsForNextTurn.get(Stat.valueOf(unique.params[1])) * unique.params[2].toFloat() / 100f
)
)
}
}
// Deprecated since 3.15.1
if (otherCiv.isCityState() && otherCiv.getDiplomacyManager(civInfo.civName).relationshipLevel() >= RelationshipLevel.Ally) {
val sciencePercentage = civInfo
.getMatchingUniques("Allied City-States provide Science equal to []% of what they produce for themselves")
.sumBy { it.params[0].toInt() }
statMap.add("City-States", Stats().apply { science = otherCiv.statsForNextTurn.science * (sciencePercentage / 100f) })
}
//
}
statMap["Transportation upkeep"] = Stats().apply { gold = -getTransportationUpkeep().toFloat() }
@ -147,9 +160,19 @@ class CivInfoStats(val civInfo: CivilizationInfo) {
for (unique in civInfo.getMatchingUniques("+1 happiness from each type of luxury resource"))
happinessPerUniqueLuxury += 1
//
statMap["Luxury resources"] = civInfo.getCivResources().map { it.resource }
.count { it.resourceType === ResourceType.Luxury } * happinessPerUniqueLuxury
val happinessBonusForCityStateProvidedLuxuries =
civInfo.getMatchingUniques("Happiness from Luxury Resources gifted by City-States increased by []%")
.map { it.params[0].toFloat() / 100f }.sum()
val luxuriesProvidedByCityStates =
civInfo.getKnownCivs().filter { it.isCityState() && it.getAllyCiv() == civInfo.civName }
.map { it.getCivResources().map { res -> res.resource } }.flatten().count { it.resourceType === ResourceType.Luxury }
statMap["City-State Luxuries"] = happinessBonusForCityStateProvidedLuxuries * luxuriesProvidedByCityStates * happinessPerUniqueLuxury
for (city in civInfo.cities) {
// There appears to be a concurrency problem? In concurrent thread in ConstructionsTable.getConstructionButtonDTOs

View File

@ -14,6 +14,7 @@ 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.metadata.GameSpeed
import com.unciv.models.ruleset.*
import com.unciv.models.ruleset.tile.ResourceSupplyList
import com.unciv.models.ruleset.tile.ResourceType
@ -27,6 +28,7 @@ import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.math.roundToInt
import kotlin.math.min
class CivilizationInfo {
@ -93,7 +95,7 @@ class CivilizationInfo {
/** See DiplomacyManager.flagsCountdown to why not eEnum */
private var flagsCountdown = HashMap<String, Int>()
/** Arraylist instead of HashMap as there might be doubles
/** Arraylist instead of HashMap as there might be doubles
* Pairs of Uniques and the amount of turns they are still active
* If the counter reaches 0 at the end of a turn, it is removed immediately
*/
@ -227,7 +229,7 @@ class CivilizationInfo {
if (resource.resourceType == ResourceType.Strategic) {
resourceModifier *= 1f + getMatchingUniques("Quantity of strategic resources produced by the empire +[]%")
.map { it.params[0].toFloat() / 100f }.sum()
// Deprecated since 3.15
if (hasUnique("Quantity of strategic resources produced by the empire increased by 100%")) {
resourceModifier *= 2f
@ -386,7 +388,7 @@ class CivilizationInfo {
* Returns a civilization caption suitable for greetings including player type info:
* Like "Milan" if the nation is a city state, "Caesar of Rome" otherwise, with an added
* " (AI)", " (Human - Hotseat)", or " (Human - Multiplayer)" if the game is multiplayer.
*/
*/
fun getLeaderDisplayName(): String {
val severalHumans = gameInfo.civilizations.count { it.playerType == PlayerType.Human } > 1
val online = gameInfo.gameParameters.isOnlineMultiplayer
@ -510,6 +512,7 @@ class CivilizationInfo {
updateViewableTiles() // adds explored tiles so that the units will be able to perform automated actions better
transients().updateCitiesConnectedToCapital()
turnStartFlags()
for (city in cities) city.startTurn()
for (unit in getCivUnits()) unit.startTurn()
@ -561,7 +564,7 @@ class CivilizationInfo {
for (city in cities.toList()) { // a city can be removed while iterating (if it's being razed) so we need to iterate over a copy
city.endTurn()
}
// Update turn counter for temporary uniques
for (unique in temporaryUniques.toList()) {
temporaryUniques.remove(unique)
@ -575,8 +578,34 @@ class CivilizationInfo {
updateHasActiveGreatWall()
}
private fun turnStartFlags() {
// This function may be too abstracted for what it currently does (only managing a single flag)
// But eh, it works.
for (flag in flagsCountdown.keys.toList()) {
if (flag == CivFlags.cityStateGreatPersonGift.name) {
val cityStateAllies = getKnownCivs().filter { it.isCityState() && it.getAllyCiv() == civName }.count()
if (cityStateAllies >= 1) flagsCountdown[flag] = flagsCountdown[flag]!! - 1
if (flagsCountdown[flag]!! < min(cityStateAllies, 10)) {
gainGreatPersonFromCityState()
flagsCountdown[flag] = turnsForGreatPersonFromCityState()
}
continue
}
if (flagsCountdown[flag]!! > 0)
flagsCountdown[flag] = flagsCountdown[flag]!! - 1
}
}
fun addFlag(flag: String, count: Int) { flagsCountdown[flag] = count }
/** Modify gold by a given amount making sure it does neither overflow nor underflow.
* @param delta the amount to add (can be negative)
* @param delta the amount to add (can be negative)
*/
fun addGold(delta: Int) {
// not using Long.coerceIn - this stays in 32 bits
@ -586,7 +615,7 @@ class CivilizationInfo {
else -> gold + delta
}
}
fun addStat(stat: Stat, amount: Int) {
when (stat) {
Stat.Culture -> policies.addCulture(amount)
@ -673,8 +702,8 @@ class CivilizationInfo {
fun influenceGainedByGift(cityState: CivilizationInfo, giftAmount: Int): Int {
var influenceGained = giftAmount / 10f
for (unique in cityState.getMatchingUniques("Gifts of Gold to City-States generate []% more Influence"))
influenceGained *= (100f + unique.params[0].toInt()) / 100
for (unique in getMatchingUniques("Gifts of Gold to City-States generate []% more Influence"))
influenceGained *= 1f + unique.params[0].toFloat() / 100f
return influenceGained.toInt()
}
@ -713,6 +742,22 @@ class CivilizationInfo {
addNotification("[${otherCiv.civName}] gave us a [${militaryUnit.name}] as gift near [${city.name}]!", locations, otherCiv.civName, militaryUnit.name)
}
/** Gain a random great person from a random city state */
private fun gainGreatPersonFromCityState() {
val givingCityState = getKnownCivs().filter { it.isCityState() && it.getAllyCiv() == civName}.random()
val giftedUnit = gameInfo.ruleSet.units.values.filter { it.isGreatPerson() }.random()
val cities = NextTurnAutomation.getClosestCities(this, givingCityState)
val placedUnit = placeUnitNearTile(cities.city1.location, giftedUnit.name)
if (placedUnit == null) return
val locations = LocationAction(listOf(placedUnit.getTile().position, cities.city2.location))
addNotification( "[${givingCityState.civName}] gave us a [${giftedUnit.name}] as a gift!", locations, givingCityState.civName, giftedUnit.name)
}
fun turnsForGreatPersonFromCityState(): Int = (40 + -2 + Random().nextInt(5)) * gameInfo.gameParameters.gameSpeed.modifier.toInt()
// There seems to be some randomness in the amount of turns between receiving each great person,
// but I have no idea what the actual lower and upper bound are, so this is just an approximation
fun getAllyCiv() = allyCivName
fun getProtectorCivs() : List<CivilizationInfo> {
@ -786,3 +831,7 @@ class CivilizationInfoPreview {
var playerId = ""
fun isPlayerCivilization() = playerType == PlayerType.Human
}
enum class CivFlags {
cityStateGreatPersonGift
}

View File

@ -4,6 +4,8 @@ import com.unciv.models.ruleset.Policy
import com.unciv.models.ruleset.Unique
import com.unciv.models.ruleset.UniqueMap
import com.unciv.models.ruleset.UniqueTriggerActivation
import com.unciv.models.translations.equalsPlaceholderText
import com.unciv.models.translations.getPlaceholderParameters
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.roundToInt
@ -168,6 +170,14 @@ class PolicyManager {
}
}
for (unique in policy.uniques) {
if (unique == "Triggers a global alert") {
triggerGlobalAlerts(policy)
} else if (unique.equalsPlaceholderText("Triggers the following global alert: []")) {
triggerGlobalAlerts(policy, unique.getPlaceholderParameters()[0])
}
}
for (unique in policy.uniqueObjects)
UniqueTriggerActivation.triggerCivwideUnique(unique, civInfo)
@ -238,4 +248,21 @@ class PolicyManager {
}
return freeBuildings
}
private fun triggerGlobalAlerts(policy: Policy, extraNotificationText: String = "") {
var extraNotificationTextCopy = extraNotificationText
if (extraNotificationText != "") {
extraNotificationTextCopy = "\n${extraNotificationText}"
}
for (civ in civInfo.gameInfo.civilizations) {
if (civ == civInfo) continue
val defaultNotificationText =
if (civ.getKnownCivs().contains(civInfo)) {
"[${civInfo.civName}] has adopted the [${policy.name}] policy"
} else {
"An unknown civilization has adopted the [${policy.name}] policy"
}
civ.addNotification("${defaultNotificationText}${extraNotificationTextCopy}", NotificationIcon.Culture)
}
}
}

View File

@ -176,13 +176,25 @@ class DiplomacyManager() {
}
return 0
}
fun matchesCityStateRelationshipFilter(filter: String): Boolean {
val relationshipLevel = relationshipLevel()
return when (filter) {
"Allied" -> relationshipLevel == RelationshipLevel.Ally
"Friendly" -> relationshipLevel == RelationshipLevel.Friend
"Enemy" -> relationshipLevel == RelationshipLevel.Enemy
"Unforgiving" -> relationshipLevel == RelationshipLevel.Unforgivable
"Neutral" -> relationshipLevel == RelationshipLevel.Neutral
else -> false
}
}
// To be run from City-State DiplomacyManager, which holds the influence. Resting point for every major civ can be different.
fun getCityStateInfluenceRestingPoint(): Float {
var restingPoint = 0f
for (unique in otherCiv().getMatchingUniques("Resting point for Influence with City-States is increased by []"))
restingPoint += unique.params[0].toInt()
if(diplomaticStatus == DiplomaticStatus.Protector) restingPoint += 5
if (diplomaticStatus == DiplomaticStatus.Protector) restingPoint += 5
return restingPoint
}
@ -203,7 +215,13 @@ class DiplomacyManager() {
}
for (unique in otherCiv().getMatchingUniques("City-State Influence degrades []% slower"))
modifier *= (100f - unique.params[0].toInt()) / 100
modifier *= 1f - unique.params[0].toFloat() / 100f
for (civ in civInfo.gameInfo.civilizations.filter { it.isMajorCiv() && it != otherCiv()}) {
for (unique in civ.getMatchingUniques("Influence of all other civilizations with all city-states degrades []% faster")) {
modifier *= 1f + unique.params[0].toFloat() / 100f
}
}
return max(0f, decrement) * max(0f, modifier)
}
@ -243,7 +261,7 @@ class DiplomacyManager() {
return goldPerTurnForUs
}
fun sciencefromResearchAgreement() {
fun scienceFromResearchAgreement() {
// https://forums.civfanatics.com/resources/research-agreements-bnw.25568/
val scienceFromResearchAgreement = min(totalOfScienceDuringRA, otherCivDiplomacy().totalOfScienceDuringRA)
civInfo.tech.scienceFromResearchAgreements += scienceFromResearchAgreement
@ -288,7 +306,7 @@ class DiplomacyManager() {
//endregion
//region state-changing functions
fun removeUntenebleTrades() {
fun removeUntenableTrades() {
for (trade in trades.toList()) {
@ -329,7 +347,7 @@ class DiplomacyManager() {
fun nextTurn() {
nextTurnTrades()
removeUntenebleTrades()
removeUntenableTrades()
updateHasOpenBorders()
nextTurnDiplomaticModifiers()
nextTurnFlags()
@ -403,7 +421,7 @@ class DiplomacyManager() {
when (flag) {
DiplomacyFlags.ResearchAgreement.name -> {
if (!otherCivDiplomacy().hasFlag(DiplomacyFlags.ResearchAgreement))
sciencefromResearchAgreement()
scienceFromResearchAgreement()
}
// This is confusingly named - in fact, the civ that has the flag set is the MAJOR civ
DiplomacyFlags.ProvideMilitaryUnit.name -> {

View File

@ -1,6 +1,7 @@
package com.unciv.models.ruleset
import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.CivFlags
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.models.stats.Stats
import com.unciv.models.translations.getPlaceholderParameters
@ -104,6 +105,23 @@ object UniqueTriggerActivation {
unit.promotions.addPromotion(promotion, isFree = true)
}
}
"Allied City-States will occasionally gift Great People" ->
civInfo.addFlag(CivFlags.cityStateGreatPersonGift.name, civInfo.turnsForGreatPersonFromCityState() / 2)
// The mechanics for granting great people are wonky, but basically the following happens:
// Based on the game speed, a timer with some amount of turns is set, 40 on regular speed
// Every turn, 1 is subtracted from this timer, as long as you have at least 1 city state ally
// So no, the number of city-state allies does not matter for this. You have a global timer for all of them combined.
// If the timer reaches the amount of city-state allies you have (or 10, whichever is lower), it is reset.
// You will then receive a random great person from a random city-state you are allied to
// The very first time after acquiring this policy, the timer is set to half of its normal value
// This is the basics, and apart from this, there is some randomness in the exact turn count, but I don't know how much
// There is surprisingly little information findable online about this policy, and the civ 5 source files are
// also quite though to search through, so this might all be incorrect.
// For now this mechanic seems decent enough that this is fine.
// Note that the way this is implemented now, this unique does NOT stack
// I could parametrize the [Allied], but eh.
}
}
}

View File

@ -320,6 +320,14 @@ Unless otherwise specified, all the following are from [the Noun Project](https:
* [Religion](https://thenounproject.com/term/religion/1307794/) By Ben Avery for Free Religion
* [Flame](https://thenounproject.com/term/flame/633228/) By Ian Shoobridge for Mandate Of Heaven
### Patronage
* Adapted from [Gold](https://thenounproject.com/term/gold/842351) by Aneeque Ahmed for Philantropy
* [Ornament](https://thenounproject.com/term/ornament/3945298) by Tommy Suhartomo for Aesthetics
* [Book Gift](https://thenounproject.com/term/book-gift/671626) by Wolf Böse for Scholasticism
* [agreement](https://thenounproject.com/term/agreement/1828960) by RomanP for Cultural Diplomacy
* [professor](https://thenounproject.com/term/professor/232239) by Andrew Doane for Educated Elite
### Commerce
* [Trade](https://thenounproject.com/term/trade/686718/) By Gregor Cresnar for Trade Unions