Made unhappiness effects moddable by adding a global uniques json; added revolts when < -20 happiness (#5932)

* Added a json file for unhappiness effects

* Change existing code to handle these effects

* Made a weird and unexpendable way to add unhappiness effects to the civilopedia

* Add the default unhappinesseffects to mods without the json

* Added revolts when at very low happiness

* Renamed a few often-used functions

* Added a file for uniques that are always active

* Fixed tests

* Nullifies [Food] -> Nullifies Growth
This commit is contained in:
Xander Lenstra 2022-01-24 18:19:51 +01:00 committed by GitHub
parent 40cd2ba24b
commit f6cb2bd0d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 325 additions and 106 deletions

View File

@ -0,0 +1,25 @@
{
"name": "Global uniques",
"uniques": [
"[-75]% growth [in all cities] <when between [-10] and [0] Happiness>",
"Nullifies Growth [in all cities] <when below [-10] Happiness>",
"[-50]% [Production] [in all cities] <when below [-10] Happiness>",
"[-33]% Strength <for [All] units> <when below [-10] Happiness>",
"Cannot build [Settler] units <when below [-10] Happiness>",
"Rebel units may spawn <when below [-20] Happiness>"
// TODO: Implement the uniques below
// "[+20]% [Culture] [in all cities] <during a golden age>",
// "[+20]% [Production] [in all cities] <during a golden age>",
// "[+10]% growth [in all cities] <during We Love The King Day>",
// "Nullifies All Yield <while is in resistance>",
// "[-25]% [Science] [in pupetted cities]" -- Imo cityFilters should ideally become conditionals anyway
// "[-25]% [Culture] [in pupetted cities]"
// "[+20]% [Production] [in cities connected via railroad]"
// something something unit supply
]
}

View File

@ -0,0 +1,25 @@
{
"name": "Global uniques",
"uniques": [
"[-75]% growth [in all cities] <when between [-10] and [0] Happiness>",
"Nullifies Growth [in all cities] <when below [-10] Happiness>",
"[-50]% [Production] [in all cities] <when below [-10] Happiness>",
"[-33]% Strength <for [All] units> <when below [-10] Happiness>",
"Cannot build [Settler] units <when below [-10] Happiness>",
"Rebel units may spawn <when below [-20] Happiness>"
// TODO: Implement the uniques below
// "[+20]% [Culture] [in all cities] <during a golden age>",
// "[+20]% [Production] [in all cities] <during a golden age>",
// "[+10]% growth [in all cities] <during We Love The King Day>",
// "Nullifies All Yield <while is in resistance>",
// "[-25]% [Science] [in pupetted cities]" -- Imo cityFilters should ideally become conditionals anyway
// "[-25]% [Culture] [in pupetted cities]"
// "[+20]% [Production] [in cities connected via railroad]"
// something something unit supply
]
}

View File

@ -33,7 +33,7 @@
"This means that it is very difficult to expand quickly in Unciv.\nIt isnt impossible, but as a new player you probably shouldn't do it.\nSo what should you do? Chill out, scout, and improve the land that you do have by building Workers.\nOnly build new cities once you have found a spot that you believe is appropriate."
],
Unhappiness: [
"It seems that your citizens are unhappy!\nWhile unhappy, cities will grow at 1/4 the speed, and your units will suffer a 2% penalty for each unhappiness",
"It seems that your citizens are unhappy!\nWhile unhappy, your civilization will suffer many detrimental effects, increasing in severity as unhappiness gets higher.",
"Unhappiness has two main causes: Population and cities.\n Each city causes 3 unhappiness, and each population, 1",
"There are 2 main ways to combat unhappiness:\n by building happiness buildings for your population\n or by having improved luxury resources within your borders."
],

View File

@ -754,6 +754,7 @@ Force =
GOLDEN AGE =
Golden Age =
We Love The King Day =
Global Effect =
[year] BC =
[year] AD =
Civilopedia =
@ -811,7 +812,6 @@ Specialist Allocation =
Specialists =
[specialist] slots =
Food eaten =
Growth bonus =
Unassigned population =
[turnsToExpansion] turns to expansion =
Stopped expansion =

View File

@ -66,13 +66,13 @@ class MainMenuScreen: BaseScreen() {
// If we were in a mod, some of the resource images for the background map we're creating
// will not exist unless we reset the ruleset and images
ImageGetter.ruleset = RulesetCache.getBaseRuleset()
ImageGetter.ruleset = RulesetCache.getVanillaRuleset()
crashHandlingThread(name = "ShowMapBackground") {
val newMap = MapGenerator(RulesetCache.getBaseRuleset())
val newMap = MapGenerator(RulesetCache.getVanillaRuleset())
.generateMap(MapParameters().apply { mapSize = MapSizeNew(MapSize.Small); type = MapType.default })
postCrashHandlingRunnable { // for GL context
ImageGetter.setNewRuleset(RulesetCache.getBaseRuleset())
ImageGetter.setNewRuleset(RulesetCache.getVanillaRuleset())
val mapHolder = EditorMapHolder(MapEditorScreen(), newMap)
backgroundTable.addAction(Actions.sequence(
Actions.fadeOut(0f),

View File

@ -117,7 +117,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
postCrashHandlingRunnable {
musicController.chooseTrack(suffix = MusicMood.Menu)
ImageGetter.ruleset = RulesetCache.getBaseRuleset() // so that we can enter the map editor without having to load a game first
ImageGetter.ruleset = RulesetCache.getVanillaRuleset() // so that we can enter the map editor without having to load a game first
if (settings.isFreshlyCreated) {
setScreen(LanguagePickerScreen())

View File

@ -40,7 +40,7 @@ object GameStarter {
gameSetupInfo.gameParameters.baseRuleset = baseRulesetInMods
if (!RulesetCache.containsKey(gameSetupInfo.gameParameters.baseRuleset))
gameSetupInfo.gameParameters.baseRuleset = RulesetCache.getBaseRuleset().name
gameSetupInfo.gameParameters.baseRuleset = RulesetCache.getVanillaRuleset().name
gameInfo.gameParameters = gameSetupInfo.gameParameters
val ruleset = RulesetCache.getComplexRuleset(gameInfo.gameParameters.mods, gameInfo.gameParameters.baseRuleset)

View File

@ -2,6 +2,7 @@ package com.unciv.logic.battle
import com.unciv.logic.map.TileInfo
import com.unciv.models.Counter
import com.unciv.models.ruleset.GlobalUniques
import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueTarget
@ -17,9 +18,10 @@ import kotlin.math.roundToInt
object BattleDamage {
private fun getModifierStringFromUnique(unique: Unique): String {
val source = when (unique.sourceObjectType) {
val source = when (unique.sourceObjectType) {
UniqueTarget.Unit -> "Unit ability"
UniqueTarget.Nation -> "National ability"
UniqueTarget.Global -> GlobalUniques.getUniqueSourceDescription(unique)
else -> "[${unique.sourceObjectName}] ([${unique.sourceObjectType?.name}])"
}
if (unique.conditionals.isEmpty()) return source
@ -58,19 +60,6 @@ object BattleDamage {
}
//https://www.carlsguides.com/strategy/civilization5/war/combatbonuses.php
val civHappiness = if (civInfo.isCityState() && civInfo.getAllyCiv() != null)
// If we are a city state with an ally we are vulnerable to their unhappiness.
min(
civInfo.gameInfo.getCivilization(civInfo.getAllyCiv()!!).getHappiness(),
civInfo.getHappiness()
)
else civInfo.getHappiness()
if (civHappiness < 0)
modifiers["Unhappiness"] = max(
2 * civHappiness,
-90
) // otherwise it could exceed -100% and start healing enemy units...
val adjacentUnits = combatant.getTile().neighbors.flatMap { it.getUnits() }
// Deprecated since 3.18.17

View File

@ -282,7 +282,7 @@ class CityInfo {
fun hasFlag(flag: CityFlags) = flagsCountdown.containsKey(flag.name)
fun getFlag(flag: CityFlags) = flagsCountdown[flag.name]!!
fun isWeLoveTheKingDay() = hasFlag(CityFlags.WeLoveTheKing)
fun isWeLoveTheKingDayActive() = hasFlag(CityFlags.WeLoveTheKing)
fun isInResistance() = hasFlag(CityFlags.Resistance)
/** @return the number of tiles 4 out from this city that could hold a city, ie how lonely this city is */

View File

@ -6,9 +6,9 @@ import com.unciv.logic.civilization.diplomacy.RelationshipLevel
import com.unciv.logic.map.RoadStatus
import com.unciv.models.Counter
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.GlobalUniques
import com.unciv.models.ruleset.ModOptionsConstants
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unique.*
import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.stats.Stat
import com.unciv.models.stats.StatMap
@ -167,15 +167,21 @@ class CityStats(val cityInfo: CityInfo) {
return stats
}
private fun getGrowthBonusFromPoliciesAndWonders(): Float {
var bonus = 0f
private fun getGrowthBonus(totalFood: Float): StatMap {
val growthSources = StatMap()
val stateForConditionals = StateForConditionals(cityInfo.civInfo, cityInfo)
// "[amount]% growth [cityFilter]"
for (unique in cityInfo.getMatchingUniques(UniqueType.GrowthPercentBonus)) {
if (!unique.conditionalsApply(cityInfo.civInfo, cityInfo)) continue
if (cityInfo.matchesFilter(unique.params[1]))
bonus += unique.params[0].toFloat()
for (unique in cityInfo.getMatchingUniques(UniqueType.GrowthPercentBonus, stateForConditionals = stateForConditionals)) {
if (!cityInfo.matchesFilter(unique.params[1])) continue
val uniqueSource =
if (unique.sourceObjectType == UniqueTarget.Global && unique.conditionals.any())
GlobalUniques.getUniqueSourceDescription(unique)
else unique.sourceObjectType?.name ?: ""
growthSources.add(uniqueSource, Stats(food = unique.params[0].toFloat()/100f * totalFood))
}
return bonus / 100
return growthSources
}
fun hasExtraAnnexUnhappiness(): Boolean {
@ -206,8 +212,13 @@ class CityStats(val cityInfo: CityInfo) {
private fun getStatsFromUniquesBySource(): StatTreeNode {
val sourceToStats = StatTreeNode()
fun addUniqueStats(unique:Unique) =
sourceToStats.addStats(unique.stats, unique.sourceObjectType?.name ?: "", unique.sourceObjectName ?: "")
fun addUniqueStats(unique:Unique) {
val uniqueSource =
if (unique.sourceObjectType == UniqueTarget.Global && unique.conditionals.any())
GlobalUniques.getUniqueSourceDescription(unique)
else unique.sourceObjectType?.name ?: ""
sourceToStats.addStats(unique.stats, uniqueSource, unique.sourceObjectName ?: "")
}
for (unique in cityInfo.getMatchingUniques(UniqueType.Stats))
addUniqueStats(unique)
@ -278,9 +289,14 @@ class CityStats(val cityInfo: CityInfo) {
private fun getStatsPercentBonusesFromUniquesBySource(currentConstruction: IConstruction):StatMap {
val sourceToStats = StatMap()
fun addUniqueStats(unique: Unique, stat:Stat, amount:Float) =
sourceToStats.add(unique.sourceObjectType?.name ?: "",
Stats().add(stat, amount))
fun addUniqueStats(unique: Unique, stat:Stat, amount:Float) {
val uniqueSource =
if (unique.sourceObjectType == UniqueTarget.Global && unique.conditionals.any())
GlobalUniques.getUniqueSourceDescription(unique)
else unique.sourceObjectType?.name ?: ""
sourceToStats.add(uniqueSource, Stats().add(stat, amount))
}
for (unique in cityInfo.getMatchingUniques(UniqueType.StatPercentBonus)) {
addUniqueStats(unique, Stat.valueOf(unique.params[1]), unique.params[0].toFloat())
@ -294,14 +310,14 @@ class CityStats(val cityInfo: CityInfo) {
val uniquesToCheck =
if (currentConstruction is Building && currentConstruction.isAnyWonder()) {
cityInfo.getMatchingUniques(UniqueType.PercentProductionWonders)
} else if (currentConstruction is Building && !currentConstruction.isAnyWonder()) {
cityInfo.getMatchingUniques(UniqueType.PercentProductionBuildings)
} else if (currentConstruction is BaseUnit) {
cityInfo.getMatchingUniques(UniqueType.PercentProductionUnits)
} else { // Science/Gold production
sequenceOf()
when {
currentConstruction is BaseUnit ->
cityInfo.getMatchingUniques(UniqueType.PercentProductionUnits)
currentConstruction is Building && currentConstruction.isAnyWonder() ->
cityInfo.getMatchingUniques(UniqueType.PercentProductionWonders)
currentConstruction is Building && !currentConstruction.isAnyWonder() ->
cityInfo.getMatchingUniques(UniqueType.PercentProductionBuildings)
else -> sequenceOf() // Science/Gold production
}
for (unique in uniquesToCheck) {
@ -321,9 +337,11 @@ class CityStats(val cityInfo: CityInfo) {
if (currentConstruction is Building
&& cityInfo.civInfo.cities.isNotEmpty()
&& cityInfo.civInfo.getCapital().cityConstructions.builtBuildings.contains(currentConstruction.name))
for(unique in cityInfo.getMatchingUniques("+25% Production towards any buildings that already exist in the Capital"))
addUniqueStats(unique, Stat.Production, 25f)
&& cityInfo.civInfo.getCapital().cityConstructions.builtBuildings.contains(currentConstruction.name)
) {
for (unique in cityInfo.getMatchingUniques("+25% Production towards any buildings that already exist in the Capital"))
addUniqueStats(unique, Stat.Production, 25f)
}
renameStatmapKeys(sourceToStats)
@ -462,8 +480,8 @@ class CityStats(val cityInfo: CityInfo) {
private fun updateBaseStatList(statsFromBuildings: StatTreeNode) {
val newBaseStatTree = StatTreeNode()
val newBaseStatList =
StatMap() // we don't edit the existing baseStatList directly, in order to avoid concurrency exceptions
// We don't edit the existing baseStatList directly, in order to avoid concurrency exceptions
val newBaseStatList = StatMap()
newBaseStatTree.addStats(Stats(
science = cityInfo.population.population.toFloat(),
@ -576,6 +594,20 @@ class CityStats(val cityInfo: CityInfo) {
entry.science *= statPercentBonusesSum.science.toPercent()
}
for ((unique, statToBeRemoved) in cityInfo.getMatchingUniques(UniqueType.NullifiesStat)
.map { it to Stat.valueOf(it.params[0]) }
.distinct()
) {
val removedAmount = newFinalStatList.values.sumOf { it[statToBeRemoved].toDouble() }
val uniqueSource =
if (unique.sourceObjectType == UniqueTarget.Global && unique.conditionals.any())
GlobalUniques.getUniqueSourceDescription(unique)
else unique.sourceObjectType?.name ?: ""
newFinalStatList.add(uniqueSource, Stats().apply { this[statToBeRemoved] = -removedAmount.toFloat() })
}
/* Okay, food calculation is complicated.
First we see how much food we generate. Then we apply production bonuses to it.
Up till here, business as usual.
@ -592,15 +624,12 @@ class CityStats(val cityInfo: CityInfo) {
if (totalFood > 0) {
// Since growth bonuses are special, (applied afterwards) they will be displayed separately in the user interface as well.
// All bonuses except We Love The King do apply even when unhappy
val foodFromGrowthBonuses = Stats(food = getGrowthBonusFromPoliciesAndWonders() * totalFood)
newFinalStatList.add("Growth bonus", foodFromGrowthBonuses)
val happiness = cityInfo.civInfo.getHappiness()
if (happiness < 0) {
// Unhappiness -75% to -100%
val foodReducedByUnhappiness = if (happiness <= -10) Stats(food = totalFood * -1)
else Stats(food = (totalFood * -3) / 4)
newFinalStatList.add("Unhappiness", foodReducedByUnhappiness)
} else if (cityInfo.isWeLoveTheKingDay()) {
val growthBonuses = getGrowthBonus(totalFood)
renameStatmapKeys(growthBonuses)
for (growthBonus in growthBonuses) {
newFinalStatList.add("${growthBonus.key} (Growth)", growthBonus.value)
}
if (cityInfo.isWeLoveTheKingDayActive() && cityInfo.civInfo.getHappiness() >= 0) {
// We Love The King Day +25%, only if not unhappy
val weLoveTheKingFood = Stats(food = totalFood / 4)
newFinalStatList.add("We Love The King Day", weLoveTheKingFood)
@ -619,10 +648,20 @@ class CityStats(val cityInfo: CityInfo) {
) {
newFinalStatList["Excess food to production"] = Stats(production = totalFood, food = -totalFood)
}
val growthNullifyingUnique = cityInfo.getMatchingUniques(UniqueType.NullifiesGrwoth).firstOrNull()
if (growthNullifyingUnique != null) {
val uniqueSource =
if (growthNullifyingUnique.sourceObjectType == UniqueTarget.Global && growthNullifyingUnique.conditionals.any())
GlobalUniques.getUniqueSourceDescription(growthNullifyingUnique)
else growthNullifyingUnique.sourceObjectType?.name ?: ""
val amountToRemove = -newFinalStatList.values.sumOf { it[Stat.Food].toDouble() }
newFinalStatList[uniqueSource] = Stats().apply { this[Stat.Food] = amountToRemove.toFloat() }
}
if (cityInfo.isInResistance())
newFinalStatList.clear() // NOPE
if (newFinalStatList.values.map { it.production }.sum() < 1) // Minimum production for things to progress
newFinalStatList["Production"] = Stats(production = 1f)
finalStatList = newFinalStatList

View File

@ -382,7 +382,8 @@ class CivilizationInfo {
else city.getAllUniquesWithNonLocalEffects()
}
fun hasUnique(uniqueType: UniqueType, stateForConditionals: StateForConditionals? = null) = getMatchingUniques(uniqueType, stateForConditionals).any()
fun hasUnique(uniqueType: UniqueType, stateForConditionals: StateForConditionals? =
StateForConditionals(this)) = getMatchingUniques(uniqueType, stateForConditionals).any()
fun hasUnique(unique: String) = getMatchingUniques(unique).any()
// Does not return local uniques, only global ones.
@ -402,6 +403,9 @@ class CivilizationInfo {
yieldAll(getEra().getMatchingUniques(uniqueType, stateForConditionals))
if (religionManager.religion != null)
yieldAll(religionManager.religion!!.getFounderUniques().filter { it.isOfType(uniqueType) })
yieldAll(gameInfo.ruleSet.globalUniques.getMatchingUniques(uniqueType, stateForConditionals))
}.filter {
it.conditionalsApply(stateForConditionals)
}
@ -424,8 +428,10 @@ class CivilizationInfo {
.asSequence()
.filter { it.placeholderText == uniqueTemplate }
)
yieldAll(gameInfo.ruleSet.globalUniques.getMatchingUniques(uniqueTemplate))
}
//region Units
fun getCivUnitsSize(): Int = units.size
fun getCivUnits(): Sequence<MapUnit> = units.asSequence()
@ -827,6 +833,7 @@ class CivilizationInfo {
updateViewableTiles() // adds explored tiles so that the units will be able to perform automated actions better
transients().updateCitiesConnectedToCapital()
startTurnFlags()
updateRevolts()
for (city in cities) city.startTurn() // Most expensive part of startTurn
for (unit in getCivUnits()) unit.startTurn()
@ -921,6 +928,11 @@ class CivilizationInfo {
if (flagsCountdown[flag]!! > 0)
flagsCountdown[flag] = flagsCountdown[flag]!! - 1
if (flagsCountdown[flag] != 0) continue
when (flag) {
CivFlags.RevoltSpawning.name -> doRevoltSpawn()
}
}
handleDiplomaticVictoryFlags()
}
@ -947,8 +959,8 @@ class CivilizationInfo {
}
fun addFlag(flag: String, count: Int) = flagsCountdown.set(flag, count)
fun removeFlag(flag: String) = flagsCountdown.remove(flag)
fun hasFlag(flag: String) = flagsCountdown.contains(flag)
fun getTurnsBetweenDiplomaticVotings() = (15 * gameInfo.gameParameters.gameSpeed.modifier).toInt() // Dunno the exact calculation, hidden in Lua files
@ -975,6 +987,65 @@ class CivilizationInfo {
fun shouldCheckForDiplomaticVictory() =
shouldShowDiplomaticVotingResults()
private fun updateRevolts() {
if (!hasUnique(UniqueType.SpawnRebels)) {
removeFlag(CivFlags.RevoltSpawning.name)
return
}
if (!hasFlag(CivFlags.RevoltSpawning.name)) {
addFlag(CivFlags.RevoltSpawning.name, max(getTurnsBeforeRevolt(),1))
return
}
}
private fun doRevoltSpawn() {
val random = Random()
val rebelCount = 1 + random.nextInt(100 + 20 * (cities.size - 1)) / 100
val spawnCity = cities.maxByOrNull { random.nextInt(it.population.population + 10) } ?: return
val spawnTile = spawnCity.getTiles().maxByOrNull { rateTileForRevoltSpawn(it) } ?: return
val unitToSpawn = gameInfo.ruleSet.units.values.asSequence().filter {
it.uniqueTo == null && it.isMelee() && it.isLandUnit()
&& !it.hasUnique(UniqueType.CannotAttack) && it.isBuildable(this)
}.maxByOrNull {
random.nextInt(1000)
} ?: return
repeat(rebelCount) {
gameInfo.tileMap.placeUnitNearTile(
spawnTile.position,
unitToSpawn.name,
gameInfo.getBarbarianCivilization()
)
}
// Will be automatically added again as long as unhappiness is still low enough
removeFlag(CivFlags.RevoltSpawning.name)
addNotification("Your citizens are revolting due to very high unhappiness!", spawnTile.position, unitToSpawn.name, "StatIcons/Malcontent")
}
// Higher is better
private fun rateTileForRevoltSpawn(tile: TileInfo): Int {
if (tile.isWater || tile.militaryUnit != null || tile.civilianUnit != null || tile.isCityCenter() || tile.isImpassible())
return -1;
var score = 10
if (tile.improvement == null) {
score += 4
if (tile.resource != null) {
score += 3
}
}
if (tile.getDefensiveBonus() > 0)
score += 4
return score
}
private fun getTurnsBeforeRevolt(): Int {
val score = ((4 + Random().nextInt(3)) * max(gameInfo.gameParameters.gameSpeed.modifier, 1f)).toInt()
return score
}
/** Modify gold by a given amount making sure it does neither overflow nor underflow.
* @param delta the amount to add (can be negative)
*/
@ -1198,7 +1269,7 @@ class CivilizationInfo {
return proximity
}
//////////////////////// City State wrapper functions ////////////////////////
/** Gain a random great person from the city state */
@ -1252,4 +1323,5 @@ enum class CivFlags {
ShouldResetDiplomaticVotes,
RecentlyBullied,
TurnsTillCallForBarbHelp,
RevoltSpawning,
}

View File

@ -0,0 +1,25 @@
package com.unciv.models.ruleset
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueTarget
import com.unciv.models.ruleset.unique.UniqueType
class GlobalUniques: RulesetObject() {
override var name = ""
override fun getUniqueTarget() = UniqueTarget.Global
override fun makeLink() = "" // No own category on Civilopedia screen
companion object {
fun getUniqueSourceDescription(unique: Unique): String {
if (unique.conditionals.none())
return "Global Effect"
return when (unique.conditionals.first().type) {
UniqueType.ConditionalGoldenAge -> "Golden Age"
UniqueType.ConditionalHappy -> "Happiness"
UniqueType.ConditionalBetweenHappiness, UniqueType.ConditionalBelowHappiness -> "Unhappiness"
else -> "Global Effect"
}
}
}
}

View File

@ -65,13 +65,12 @@ class Ruleset {
private val jsonParser = JsonParser()
var modWithReligionLoaded = false
var name = ""
val beliefs = LinkedHashMap<String, Belief>()
val buildings = LinkedHashMap<String, Building>()
val difficulties = LinkedHashMap<String, Difficulty>()
val eras = LinkedHashMap<String, Era>()
var globalUniques = GlobalUniques()
val nations = LinkedHashMap<String, Nation>()
val policies = LinkedHashMap<String, Policy>()
val policyBranches = LinkedHashMap<String, PolicyBranch>()
@ -104,15 +103,16 @@ class Ruleset {
}
fun add(ruleset: Ruleset) {
beliefs.putAll(ruleset.beliefs)
buildings.putAll(ruleset.buildings)
for (buildingToRemove in ruleset.modOptions.buildingsToRemove) buildings.remove(buildingToRemove)
difficulties.putAll(ruleset.difficulties)
eras.putAll(ruleset.eras)
globalUniques = GlobalUniques().apply { uniques.addAll(globalUniques.uniques); uniques.addAll(ruleset.globalUniques.uniques) }
nations.putAll(ruleset.nations)
for (nationToRemove in ruleset.modOptions.nationsToRemove) nations.remove(nationToRemove)
policyBranches.putAll(ruleset.policyBranches)
policies.putAll(ruleset.policies)
beliefs.putAll(ruleset.beliefs)
quests.putAll(ruleset.quests)
religions.addAll(ruleset.religions)
ruinRewards.putAll(ruleset.ruinRewards)
@ -127,7 +127,6 @@ class Ruleset {
unitTypes.putAll(ruleset.unitTypes)
for (unitToRemove in ruleset.modOptions.unitsToRemove) units.remove(unitToRemove)
mods += ruleset.mods
modWithReligionLoaded = modWithReligionLoaded || ruleset.modWithReligionLoaded
}
fun clear() {
@ -135,14 +134,15 @@ class Ruleset {
buildings.clear()
difficulties.clear()
eras.clear()
policyBranches.clear()
specialists.clear()
globalUniques = GlobalUniques()
mods.clear()
nations.clear()
policies.clear()
policyBranches.clear()
quests.clear()
religions.clear()
ruinRewards.clear()
quests.clear()
specialists.clear()
technologies.clear()
terrains.clear()
tileImprovements.clear()
@ -150,7 +150,6 @@ class Ruleset {
unitPromotions.clear()
units.clear()
unitTypes.clear()
modWithReligionLoaded = false
}
@ -201,7 +200,7 @@ class Ruleset {
if (erasFile.exists()) eras += createHashmap(jsonParser.getFromJson(Array<Era>::class.java, erasFile))
// While `eras.values.toList()` might seem more logical, eras.values is a MutableCollection and
// therefore does not guarantee keeping the order of elements like a LinkedHashMap does.
// Using a map sidesteps this problem
// Using map{} sidesteps this problem
eras.map { it.value }.withIndex().forEach { it.value.eraNumber = it.index }
val unitTypesFile = folderHandle.child("UnitTypes.json")
@ -254,8 +253,14 @@ class Ruleset {
}
val difficultiesFile = folderHandle.child("Difficulties.json")
if (difficultiesFile.exists()) difficulties += createHashmap(jsonParser.getFromJson(Array<Difficulty>::class.java, difficultiesFile))
if (difficultiesFile.exists())
difficulties += createHashmap(jsonParser.getFromJson(Array<Difficulty>::class.java, difficultiesFile))
val globalUniquesFile = folderHandle.child("GlobalUniques.json")
if (globalUniquesFile.exists()) {
globalUniques = jsonParser.getFromJson(GlobalUniques::class.java, globalUniquesFile)
}
val gameBasicsLoadTime = System.currentTimeMillis() - gameBasicsStartTime
if (printOutput) println("Loading ruleset - " + gameBasicsLoadTime + "ms")
}
@ -274,9 +279,7 @@ class Ruleset {
}
}
}
fun hasReligion() = beliefs.any() && modWithReligionLoaded
/** Used for displaying a RuleSet's name */
override fun toString() = when {
name.isNotEmpty() -> name
@ -476,7 +479,7 @@ class Ruleset {
// Quit here when no base ruleset is loaded - references cannot be checked
if (!modOptions.isBaseRuleset) return lines
val baseRuleset = RulesetCache.getBaseRuleset() // for UnitTypes fallback
val baseRuleset = RulesetCache.getVanillaRuleset() // for UnitTypes fallback
for (unit in units.values) {
if (unit.requiredTech != null && !technologies.containsKey(unit.requiredTech!!))
@ -697,7 +700,7 @@ object RulesetCache : HashMap<String,Ruleset>() {
}
fun getBaseRuleset() = this[BaseRuleset.Civ_V_Vanilla.fullName]!!.clone() // safeguard, so no-one edits the base ruleset by mistake
fun getVanillaRuleset() = this[BaseRuleset.Civ_V_Vanilla.fullName]!!.clone() // safeguard, so no-one edits the base ruleset by mistake
fun getSortedBaseRulesets(): List<String> {
val baseRulesets = values
@ -729,7 +732,7 @@ object RulesetCache : HashMap<String,Ruleset>() {
val baseRuleset =
if (containsKey(optionalBaseRuleset) && this[optionalBaseRuleset]!!.modOptions.isBaseRuleset) this[optionalBaseRuleset]!!
else getBaseRuleset()
else getVanillaRuleset()
val loadedMods = mods
@ -744,20 +747,20 @@ object RulesetCache : HashMap<String,Ruleset>() {
if (mod.modOptions.isBaseRuleset) {
newRuleset.modOptions = mod.modOptions
}
if (mod.beliefs.any()) {
newRuleset.modWithReligionLoaded = true
}
}
newRuleset.updateBuildingCosts() // only after we've added all the mods can we calculate the building costs
// This one should be temporary
if (newRuleset.unitTypes.isEmpty()) {
newRuleset.unitTypes.putAll(getBaseRuleset().unitTypes)
newRuleset.unitTypes.putAll(getVanillaRuleset().unitTypes)
}
// This one should be permanent
// These should be permanent
if (newRuleset.ruinRewards.isEmpty()) {
newRuleset.ruinRewards.putAll(getBaseRuleset().ruinRewards)
newRuleset.ruinRewards.putAll(getVanillaRuleset().ruinRewards)
}
if (newRuleset.globalUniques.uniques.isEmpty()) {
newRuleset.globalUniques = getVanillaRuleset().globalUniques
}
return newRuleset

View File

@ -58,6 +58,12 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s
UniqueType.ConditionalNotWar -> state.civInfo?.isAtWar() == false
UniqueType.ConditionalHappy ->
state.civInfo != null && state.civInfo.statsForNextTurn.happiness >= 0
UniqueType.ConditionalBetweenHappiness ->
state.civInfo != null
&& condition.params[0].toInt() <= state.civInfo.happinessForNextTurn
&& state.civInfo.happinessForNextTurn < condition.params[1].toInt()
UniqueType.ConditionalBelowHappiness ->
state.civInfo != null && state.civInfo.happinessForNextTurn < condition.params[0].toInt()
UniqueType.ConditionalGoldenAge ->
state.civInfo != null && state.civInfo.goldenAges.isGoldenAge()
UniqueType.ConditionalBeforeEra ->

View File

@ -87,6 +87,7 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
// Stat percentage boosts
StatPercentBonus("[amount]% [stat]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
StatPercentBonusCities("[amount]% [stat] [cityFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
StatPercentFromObject("[amount]% [stat] from every [tileFilter/specialist/buildingName]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
@Deprecated("As of 3.18.17", ReplaceWith("[amount]% [stat] from every [tileFilter/specialist/buildingName]"))
StatPercentSignedFromObject("+[amount]% [stat] from every [tileFilter/specialist/buildingName]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
@ -95,7 +96,8 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
AllStatsSignedPercentFromObject("+[amount]% yield from every [tileFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
StatPercentFromReligionFollowers("[amount]% [stat] from every follower, up to [amount]%", UniqueTarget.FollowerBelief),
BonusStatsFromCityStates("[amount]% [stat] from City-States", UniqueTarget.Global),
StatPercentBonusCities("[amount]% [stat] [cityFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
NullifiesStat("Nullifies [stat] [cityFilter]", UniqueTarget.Global),
NullifiesGrwoth("Nullifies Growth [cityFilter]", UniqueTarget.Global),
PercentProductionWonders("[amount]% Production when constructing [buildingFilter] wonders [cityFilter]", UniqueTarget.Global, UniqueTarget.Resource, UniqueTarget.FollowerBelief),
PercentProductionBuildings("[amount]% Production when constructing [buildingFilter] buildings [cityFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
@ -266,6 +268,8 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
StartsWithTech("Starts with [tech]", UniqueTarget.Nation),
ResearchableMultipleTimes("Can be continually researched", UniqueTarget.Global),
SpawnRebels("Rebel units may spawn", UniqueTarget.Global),
//endregion
//endregion Global uniques
@ -514,8 +518,11 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
/////// civ conditionals
ConditionalWar("when at war", UniqueTarget.Conditional),
ConditionalNotWar("when not at war", UniqueTarget.Conditional),
ConditionalHappy("while the empire is happy", UniqueTarget.Conditional),
ConditionalGoldenAge("during a Golden Age", UniqueTarget.Conditional),
ConditionalHappy("while the empire is happy", UniqueTarget.Conditional),
ConditionalBetweenHappiness("when between [amount] and [amount] Happiness", UniqueTarget.Conditional),
ConditionalBelowHappiness("when below [amount] Happiness", UniqueTarget.Conditional),
ConditionalDuringEra("during the [era]", UniqueTarget.Conditional),
ConditionalBeforeEra("before the [era]", UniqueTarget.Conditional),

View File

@ -238,7 +238,7 @@ object TranslationFileWriter {
private fun generateStringsFromJSONs(jsonsFolder: FileHandle): LinkedHashMap<String, MutableSet<String>> {
// build maps identifying parameters as certain types of filters - unitFilter etc
val ruleset = RulesetCache.getBaseRuleset()
val ruleset = RulesetCache.getVanillaRuleset()
val tileFilterMap = ruleset.terrains.keys.toMutableSet().apply { addAll(sequenceOf(
"Friendly Land",
"Foreign Land",
@ -422,6 +422,7 @@ object TranslationFileWriter {
"Buildings" -> emptyArray<Building>().javaClass
"Difficulties" -> emptyArray<Difficulty>().javaClass
"Eras" -> emptyArray<Era>().javaClass
"GlobalUniques" -> GlobalUniques().javaClass
"Nations" -> emptyArray<Nation>().javaClass
"Policies" -> emptyArray<PolicyBranch>().javaClass
"Quests" -> emptyArray<Quest>().javaClass

View File

@ -87,7 +87,7 @@ class CityStatsTable(val cityScreen: CityScreen): Table() {
tableWithIcons.add(ImageGetter.getImage("StatIcons/Resistance")).size(20f)
tableWithIcons.add("In resistance for another [${cityInfo.getFlag(CityFlags.Resistance)}] turns".toLabel()).row()
}
if (cityInfo.isWeLoveTheKingDay()) {
if (cityInfo.isWeLoveTheKingDayActive()) {
tableWithIcons.add(ImageGetter.getStatIcon("Food")).size(20f)
tableWithIcons.add("We Love The King Day for another [${cityInfo.getFlag(CityFlags.WeLoveTheKing)}] turns".toLabel()).row()
} else if (cityInfo.demandedResource != "") {

View File

@ -196,7 +196,7 @@ class CivilopediaScreen(
CivilopediaCategories.Technology -> ruleset.technologies.values
CivilopediaCategories.Promotion -> ruleset.unitPromotions.values
CivilopediaCategories.Policy -> ruleset.policies.values
CivilopediaCategories.Tutorial -> tutorialController.getCivilopediaTutorials()
CivilopediaCategories.Tutorial -> tutorialController.getCivilopediaTutorials(ruleset)
CivilopediaCategories.Difficulty -> ruleset.difficulties.values
CivilopediaCategories.Belief -> (ruleset.beliefs.values.asSequence() +
Belief.getCivilopediaReligionEntry(ruleset)).toList()

View File

@ -22,7 +22,7 @@ import com.unciv.ui.utils.*
class MapEditorScreen(): BaseScreen() {
var mapName = ""
var tileMap = TileMap()
var ruleset = Ruleset().apply { add(RulesetCache.getBaseRuleset()) }
var ruleset = Ruleset().apply { add(RulesetCache.getVanillaRuleset()) }
var gameSetupInfo = GameSetupInfo()
lateinit var mapHolder: EditorMapHolder

View File

@ -23,7 +23,7 @@ import com.unciv.ui.utils.AutoScrollPane as ScrollPane
/** New map generation screen */
class NewMapScreen(val mapParameters: MapParameters = getDefaultParameters()) : PickerScreen() {
private val ruleset = RulesetCache.getBaseRuleset()
private val ruleset = RulesetCache.getVanillaRuleset()
private var generatedMap: TileMap? = null
private val mapParametersTable: MapParametersTable
private val modCheckBoxes: ModCheckboxTable

View File

@ -4,6 +4,7 @@ import com.badlogic.gdx.utils.Array
import com.unciv.JsonParser
import com.unciv.UncivGame
import com.unciv.models.Tutorial
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.stats.INamed
import com.unciv.ui.civilopedia.FormattedLine
import com.unciv.ui.civilopedia.SimpleCivilopediaText
@ -70,10 +71,13 @@ class TutorialController(screen: BaseScreen) {
/** Get all Tutorials intended to be displayed in the Civilopedia
* as a List of wrappers supporting INamed and ICivilopediaText
*/
fun getCivilopediaTutorials() =
tutorials.filter {
fun getCivilopediaTutorials(ruleset: Ruleset): List<CivilopediaTutorial> {
val civilopediaTutorials = tutorials.filter {
Tutorial.findByName(it.key)!!.isCivilopedia
}.map {
CivilopediaTutorial(it.key, it.value)
}.map { tutorial ->
val lines = tutorial.value
CivilopediaTutorial(tutorial.key, lines)
}
return civilopediaTutorials
}
}

View File

@ -76,6 +76,11 @@ Example: "[20]% [Culture]"
Applicable to: Global, FollowerBelief
#### [amount]% [stat] [cityFilter]
Example: "[20]% [Culture] [in all cities]"
Applicable to: Global, FollowerBelief
#### [amount]% [stat] from every [tileFilter/specialist/buildingName]
Example: "[20]% [Culture] from every [tileFilter/specialist/buildingName]"
@ -91,10 +96,15 @@ Example: "[20]% [Culture] from City-States"
Applicable to: Global
#### [amount]% [stat] [cityFilter]
Example: "[20]% [Culture] [in all cities]"
#### Nullifies [stat] [cityFilter]
Example: "Nullifies [Culture] [in all cities]"
Applicable to: Global, FollowerBelief
Applicable to: Global
#### Nullifies Growth [cityFilter]
Example: "Nullifies Growth [in all cities]"
Applicable to: Global
#### [amount]% Production when constructing [buildingFilter] wonders [cityFilter]
Example: "[20]% Production when constructing [Culture] wonders [in all cities]"
@ -388,6 +398,9 @@ Applicable to: Global
#### Can be continually researched
Applicable to: Global
#### Rebel units may spawn
Applicable to: Global
#### [amount]% Strength
Example: "[20]% Strength"
@ -1206,10 +1219,20 @@ Applicable to: Conditional
#### <when not at war>
Applicable to: Conditional
#### <during a Golden Age>
Applicable to: Conditional
#### <while the empire is happy>
Applicable to: Conditional
#### <during a Golden Age>
#### <when between [amount] and [amount] Happiness>
Example: "<when between [20] and [20] Happiness>"
Applicable to: Conditional
#### <when below [amount] Happiness>
Example: "<when below [20] Happiness>"
Applicable to: Conditional
#### <during the [era]>

View File

@ -31,7 +31,7 @@ class TileImprovementConstructionTests {
@Before
fun initTheWorld() {
RulesetCache.loadRulesets()
ruleSet = RulesetCache.getBaseRuleset()
ruleSet = RulesetCache.getVanillaRuleset()
civInfo.tech.researchedTechnologies.addAll(ruleSet.technologies.values)
civInfo.tech.techsResearched.addAll(ruleSet.technologies.keys)
city.civInfo = civInfo

View File

@ -22,7 +22,7 @@ class TileMapTests {
@Before
fun initTheWorld() {
RulesetCache.loadRulesets()
ruleSet = RulesetCache.getBaseRuleset()
ruleSet = RulesetCache.getVanillaRuleset()
map = TileMap()
tile1.position = Vector2(0f, 0f)

View File

@ -28,7 +28,7 @@ class UnitMovementAlgorithmsTests {
@Before
fun initTheWorld() {
RulesetCache.loadRulesets()
ruleSet = RulesetCache.getBaseRuleset()
ruleSet = RulesetCache.getVanillaRuleset()
tile.ruleset = ruleSet
tile.baseTerrain = Constants.grassland
civInfo.tech.techsResearched.addAll(ruleSet.technologies.keys)

View File

@ -28,7 +28,7 @@ class BasicTests {
@Before
fun loadTranslations() {
RulesetCache.loadRulesets()
ruleset = RulesetCache.getBaseRuleset()
ruleset = RulesetCache.getVanillaRuleset()
}
@Test

View File

@ -31,7 +31,7 @@ class TranslationTests {
}))
translations.readAllLanguagesTranslation()
RulesetCache.loadRulesets()
ruleset = RulesetCache.getBaseRuleset()
ruleset = RulesetCache.getVanillaRuleset()
System.setOut(outputChannel)
}

View File

@ -41,7 +41,7 @@ class GlobalUniquesTests {
// Create a new ruleset we can easily edit, and set the important variables of gameInfo
RulesetCache.loadRulesets()
ruleSet = RulesetCache.getBaseRuleset()
ruleSet = RulesetCache.getVanillaRuleset()
gameInfo.ruleSet = ruleSet
gameInfo.difficultyObject = ruleSet.difficulties["Prince"]!!