diff --git a/android/assets/jsons/Civ V - Vanilla/Ruins.json b/android/assets/jsons/Civ V - Vanilla/Ruins.json index 05bbfb577d..1ddf8e5ea0 100644 --- a/android/assets/jsons/Civ V - Vanilla/Ruins.json +++ b/android/assets/jsons/Civ V - Vanilla/Ruins.json @@ -1,64 +1,70 @@ [ { - "name": "freeCulture", + "name": "discover cultural artifacts", "notification": "We have discovered cultural artifacts in the ruins! (+20 culture)", - "uniques": ["Gain [20] [Culture]"] + "uniques": ["Gain [20] [Culture]"], + "color": "#cf8ff7" }, { - "name": "joinWorker", + "name": "squatters willing to work for you", "notification": "A [Worker] has joined us!", "uniques": ["Free [Worker] found in the ruins"], "excludedDifficulties": ["Prince", "King", "Emperor", "Immortal", "Deity"] }, { - "name": "joinSettler", + "name": "squatters wishing to settle under your rule", "notification": "A [Settler] has joined us!", "uniques": ["Free [Settler] found in the ruins"], "excludedDifficulties": ["Warlord","Prince","King","Emperor","Immortal","Deity"] }, { - "name": "freeXP", + "name": "your exploring unit receives training", "notification": "An ancient tribe trained us in their ways of combat!", "uniques": ["This Unit gains [10] XP"] }, { - "name": "freePop", + "name": "survivors (adds population to a city)", "notification": "We have found survivors in the ruins! Population added to [cityName].", - "uniques": ["[+1] population in a random city"] // This can't be easily added to cityFilter, as it is non-deterministic + "uniques": ["[+1] population in a random city"], // This can't be easily added to cityFilter, as it is non-deterministic + "color": "#81c784" }, { - "name": "freeGold", + "name": "a stash of gold", "notification": "We have found a stash of [goldAmount] Gold in the ruins!", - "uniques": ["Gain [50]-[100] [Gold]"] + "uniques": ["Gain [50]-[100] [Gold]"], + "color": "#ffeb7f" }, { - "name": "freeAncientTech", + "name": "discover a lost technology", "notification": "We have discovered the lost technology of [techName] in the ruins!", - "uniques": ["[1] free random researchable Tech(s) from the [Ancient era]"] + "uniques": ["[1] free random researchable Tech(s) from the [Ancient era]"], + "color": "#7f7fff" }, { - "name": "unitUpgrade", + "name": "advanced weaponry for your explorer", "notification": "Our unit finds advanced weaponry hidden in the ruins!", "uniques": ["This Unit upgrades for free including special upgrades"] }, { - "name": "barbCampsRevealed", + "name": "reveal nearby Barbarian camps", "notification": "You find evidence of Barbarian activity. Nearby Barbarian camps are revealed!", "uniques": ["Reveal up to [All] [Barbarian encampment] within a [10] tile radius"] }, { - "name": "crudelyDrawnMap", + "name": "find a crudely-drawn map", "notification": "We have found a crudely-drawn map in the ruins!", "uniques": ["From a randomly chosen tile [4] tiles away, reveal tiles up to [4] tiles away with [80]% chance"] }, { - "name": "holySymbols", + "name": "discover holy symbols", "notification": "We have found holy symbols in the ruins, giving us a deeper understanding of religion! (+[faithAmount] Faith)", - "uniques": ["Hidden when religion is disabled", "Gain enough Faith for a Pantheon"] + "uniques": ["Hidden when religion is disabled", "Gain enough Faith for a Pantheon"], + "color": "#CDDDF4" }, { - "name": "prophecy", + "name": "an ancient prophecy", "notification": "We have found an ancient prophecy in the ruins, greatly increasing our spiritual connection! (+[faithAmount] Faith)", - "uniques": ["Hidden when religion is disabled", "Gain enough Faith for [33]% of a Great Prophet", "Hidden after generating a Great Prophet"] + "uniques": ["Hidden when religion is disabled", "Gain enough Faith for [33]% of a Great Prophet", "Hidden after generating a Great Prophet"], + "color": "#CDDDF4" } ] \ No newline at end of file diff --git a/android/assets/jsons/Civ V - Vanilla/TileImprovements.json b/android/assets/jsons/Civ V - Vanilla/TileImprovements.json index e434485196..de53494ec9 100644 --- a/android/assets/jsons/Civ V - Vanilla/TileImprovements.json +++ b/android/assets/jsons/Civ V - Vanilla/TileImprovements.json @@ -237,19 +237,7 @@ { "name": "Ancient ruins", - "uniques": ["Unpillagable", "Provides a random bonus when entered"], - "civilopediaText": [ - {"text":"Ancient ruins provide a one-time random bonus when explored"}, - {}, - {"text":"The possible rewards are:"}, - {"text":"a stash of gold", "indent":1, "starred":true, "color":"#ffeb7f"}, - {"text":"survivors (adds population to a city)", "indent":1, "starred":true, "color":"#81c784"}, - {"text":"discover a lost technology", "indent":1, "starred":true, "color":"#7f7fff"}, - {"text":"a unit joins you", "indent":1, "starred":true}, - {"text":"your exploring unit receives training", "indent":1, "starred":true}, // "link":"Tutorial/Experience" - {"text":"discover cultural artifacts", "indent":1, "starred":true, "color":"#cf8ff7"}, - {"text":"find a crudely-drawn map", "indent":1, "starred":true} - ] + "uniques": ["Unpillagable", "Provides a random bonus when entered"] }, { "name": "City ruins", diff --git a/core/src/com/unciv/Constants.kt b/core/src/com/unciv/Constants.kt index 589ce086f5..185d81b803 100644 --- a/core/src/com/unciv/Constants.kt +++ b/core/src/com/unciv/Constants.kt @@ -9,6 +9,8 @@ object Constants { const val settler = "Settler" const val settlerUnique = "Founds a new city" const val eraSpecificUnit = "Era Starting Unit" + const val hiddenWithoutReligionUnique = "Hidden when religion is disabled" + const val hideFromCivilopediaUnique = "Will not be displayed in Civilopedia" const val impassable = "Impassable" const val ocean = "Ocean" diff --git a/core/src/com/unciv/logic/civilization/RuinsManager/RuinsManager.kt b/core/src/com/unciv/logic/civilization/RuinsManager/RuinsManager.kt index ae54f4ca06..c3f4d2064e 100644 --- a/core/src/com/unciv/logic/civilization/RuinsManager/RuinsManager.kt +++ b/core/src/com/unciv/logic/civilization/RuinsManager/RuinsManager.kt @@ -1,5 +1,6 @@ package com.unciv.logic.civilization.RuinsManager +import com.unciv.Constants import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.MapUnit import com.unciv.models.ruleset.RuinReward @@ -40,7 +41,7 @@ class RuinsManager { for (possibleReward in possibleRewards) { if (civInfo.gameInfo.difficulty in possibleReward.excludedDifficulties) continue - if ("Hidden when religion is disabled" in possibleReward.uniques && !civInfo.gameInfo.hasReligionEnabled()) continue + if (Constants.hiddenWithoutReligionUnique in possibleReward.uniques && !civInfo.gameInfo.hasReligionEnabled()) continue if ("Hidden after generating a Great Prophet" in possibleReward.uniques && civInfo.religionManager.greatProphetsEarned > 0) continue if (possibleReward.uniqueObjects.any { unique -> unique.placeholderText == "Only available after [] turns" diff --git a/core/src/com/unciv/models/ruleset/Building.kt b/core/src/com/unciv/models/ruleset/Building.kt index f5b971955c..bba420990d 100644 --- a/core/src/com/unciv/models/ruleset/Building.kt +++ b/core/src/com/unciv/models/ruleset/Building.kt @@ -1,5 +1,6 @@ package com.unciv.models.ruleset +import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.city.CityConstructions import com.unciv.logic.city.CityInfo @@ -117,7 +118,7 @@ class Building : NamedStats(), INonPerpetualConstruction, ICivilopediaText { .filterNot { it.startsWith("Hidden ") && it.endsWith(" disabled") || it == "Unbuildable" || - it == "Will not be displayed in Civilopedia" + it == Constants.hideFromCivilopediaUnique } /** used in CityScreen (CityInfoTable and ConstructionInfoTable) */ @@ -468,7 +469,7 @@ class Building : NamedStats(), INonPerpetualConstruction, ICivilopediaText { "Can only be built in annexed cities" -> if (construction.cityInfo.isPuppet || construction.cityInfo.foundingCiv == "" || construction.cityInfo.civInfo.civName == construction.cityInfo.foundingCiv) return unique.text "Obsolete with []" -> if (civInfo.tech.isResearched(unique.params[0])) return unique.text - "Hidden when religion is disabled" -> if (!civInfo.gameInfo.hasReligionEnabled()) return unique.text + Constants.hiddenWithoutReligionUnique -> if (!civInfo.gameInfo.hasReligionEnabled()) return unique.text } if (uniqueTo != null && uniqueTo != civInfo.civName) return "Unique to $uniqueTo" diff --git a/core/src/com/unciv/models/ruleset/Nation.kt b/core/src/com/unciv/models/ruleset/Nation.kt index 759a039914..685c1e6f76 100644 --- a/core/src/com/unciv/models/ruleset/Nation.kt +++ b/core/src/com/unciv/models/ruleset/Nation.kt @@ -119,7 +119,7 @@ class Nation : INamed, ICivilopediaText, IHasUniques { private fun addUniqueBuildingsText(textList: ArrayList, ruleset: Ruleset) { for (building in ruleset.buildings.values - .filter { it.uniqueTo == name && "Will not be displayed in Civilopedia" !in it.uniques }) { + .filter { it.uniqueTo == name && Constants.hideFromCivilopediaUnique !in it.uniques }) { if (building.replaces != null && ruleset.buildings.containsKey(building.replaces!!)) { val originalBuilding = ruleset.buildings[building.replaces!!]!! @@ -148,7 +148,7 @@ class Nation : INamed, ICivilopediaText, IHasUniques { private fun addUniqueUnitsText(textList: ArrayList, ruleset: Ruleset) { for (unit in ruleset.units.values - .filter { it.uniqueTo == name && "Will not be displayed in Civilopedia" !in it.uniques }) { + .filter { it.uniqueTo == name && Constants.hideFromCivilopediaUnique !in it.uniques }) { if (unit.replaces != null && ruleset.units.containsKey(unit.replaces!!)) { val originalUnit = ruleset.units[unit.replaces!!]!! textList += unit.name.tr() + " - " + "Replaces [${originalUnit.name}]".tr() @@ -239,7 +239,7 @@ class Nation : INamed, ICivilopediaText, IHasUniques { @JvmName("addUniqueBuildingsText1") // These overloads are too similar - but I hope to remove the other one soon private fun addUniqueBuildingsText(textList: ArrayList, ruleset: Ruleset) { for (building in ruleset.buildings.values) { - if (building.uniqueTo != name || "Will not be displayed in Civilopedia" in building.uniques) continue + if (building.uniqueTo != name || Constants.hideFromCivilopediaUnique in building.uniques) continue textList += FormattedLine("{${building.name}} -", link=building.makeLink()) if (building.replaces != null && ruleset.buildings.containsKey(building.replaces!!)) { val originalBuilding = ruleset.buildings[building.replaces!!]!! @@ -275,7 +275,7 @@ class Nation : INamed, ICivilopediaText, IHasUniques { @JvmName("addUniqueUnitsText1") private fun addUniqueUnitsText(textList: ArrayList, ruleset: Ruleset) { for (unit in ruleset.units.values) { - if (unit.uniqueTo != name || "Will not be displayed in Civilopedia" in unit.uniques) continue + if (unit.uniqueTo != name || Constants.hideFromCivilopediaUnique in unit.uniques) continue textList += FormattedLine("{${unit.name}} -", link="Unit/${unit.name}") if (unit.replaces != null && ruleset.units.containsKey(unit.replaces!!)) { val originalUnit = ruleset.units[unit.replaces!!]!! diff --git a/core/src/com/unciv/models/ruleset/RuinReward.kt b/core/src/com/unciv/models/ruleset/RuinReward.kt index 60b7a17e3c..81a2b367f3 100644 --- a/core/src/com/unciv/models/ruleset/RuinReward.kt +++ b/core/src/com/unciv/models/ruleset/RuinReward.kt @@ -1,12 +1,19 @@ package com.unciv.models.ruleset import com.unciv.models.stats.INamed +import com.unciv.ui.civilopedia.FormattedLine +import com.unciv.ui.civilopedia.ICivilopediaText -class RuinReward : INamed { - override lateinit var name: String +class RuinReward : INamed, ICivilopediaText { + override lateinit var name: String // Displayed in Civilopedia! val notification: String = "" val uniques: List = listOf() + @delegate:Transient // Defense in depth against mad modders val uniqueObjects: List by lazy { uniques.map { Unique(it) } } val excludedDifficulties: List = listOf() val weight: Int = 1 -} \ No newline at end of file + val color: String = "" // For Civilopedia + + override var civilopediaText = listOf() + override fun makeLink() = "" //No own category on Civilopedia screen +} diff --git a/core/src/com/unciv/models/ruleset/tech/Technology.kt b/core/src/com/unciv/models/ruleset/tech/Technology.kt index e570535284..35d6f3c653 100644 --- a/core/src/com/unciv/models/ruleset/tech/Technology.kt +++ b/core/src/com/unciv/models/ruleset/tech/Technology.kt @@ -1,5 +1,6 @@ package com.unciv.models.ruleset.tech +import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.civilization.CivilizationInfo import com.unciv.models.ruleset.Building @@ -117,8 +118,8 @@ class Technology: INamed, ICivilopediaText, IHasUniques { predicate(it) // expected to be the most selective, thus tested first && (it.uniqueTo == civInfo.civName || it.uniqueTo==null && civInfo.getEquivalentBuilding(it) == it) && (nuclearWeaponsEnabled || "Enables nuclear weapon" !in it.uniques) - && (religionEnabled || "Hidden when religion is disabled" !in it.uniques) - && "Will not be displayed in Civilopedia" !in it.uniques + && (religionEnabled || Constants.hiddenWithoutReligionUnique !in it.uniques) + && Constants.hideFromCivilopediaUnique !in it.uniques } } @@ -136,8 +137,8 @@ class Technology: INamed, ICivilopediaText, IHasUniques { it.requiredTech == name && (it.uniqueTo == civInfo.civName || it.uniqueTo==null && civInfo.getEquivalentUnit(it) == it) && (nuclearWeaponsEnabled || it.uniqueObjects.none { unique -> unique.placeholderText == "Nuclear weapon of Strength []" }) - && (religionEnabled || "Hidden when religion is disabled" !in it.uniques) - && "Will not be displayed in Civilopedia" !in it.uniques + && (religionEnabled || Constants.hiddenWithoutReligionUnique !in it.uniques) + && Constants.hideFromCivilopediaUnique !in it.uniques } } diff --git a/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt b/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt index d612dc18bd..364c8dbbbc 100644 --- a/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt +++ b/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt @@ -1,6 +1,9 @@ package com.unciv.models.ruleset.tile +import com.unciv.Constants +import com.unciv.UncivGame import com.unciv.logic.civilization.CivilizationInfo +import com.unciv.logic.civilization.RuinsManager.RuinsManager import com.unciv.models.ruleset.Belief import com.unciv.logic.map.RoadStatus import com.unciv.models.ruleset.IHasUniques @@ -167,6 +170,22 @@ class TileImprovement : NamedStats(), ICivilopediaText, IHasUniques { textList += FormattedLine(unique) } + if (isAncientRuinsEquivalent() && ruleset.ruinRewards.isNotEmpty()) { + val difficulty = UncivGame.Current.gameInfo.gameParameters.difficulty + val religionEnabled = UncivGame.Current.gameInfo.hasReligionEnabled() + textList += FormattedLine() + textList += FormattedLine("The possible rewards are:") + ruleset.ruinRewards.values.asSequence() + .filter { reward -> + difficulty !in reward.excludedDifficulties && + (religionEnabled || Constants.hiddenWithoutReligionUnique !in reward.uniques) + } + .forEach { reward -> + textList += FormattedLine(reward.name, starred = true, color = reward.color) + textList += reward.civilopediaText + } + } + val unit = ruleset.units.asSequence().firstOrNull { entry -> entry.value.uniques.any { it.startsWith("Can construct [$name]") diff --git a/core/src/com/unciv/models/translations/TranslationFileWriter.kt b/core/src/com/unciv/models/translations/TranslationFileWriter.kt index 1269f7671a..d84db855fa 100644 --- a/core/src/com/unciv/models/translations/TranslationFileWriter.kt +++ b/core/src/com/unciv/models/translations/TranslationFileWriter.kt @@ -48,7 +48,7 @@ object TranslationFileWriter { if (modFolder == null) { // base game val templateFile = getFileHandle(modFolder, templateFileLocation) // read the template if (templateFile.exists()) - linesFromTemplates.addAll(templateFile.reader().readLines()) + linesFromTemplates.addAll(templateFile.reader(TranslationFileReader.charset).readLines()) for (baseRuleset in BaseRuleset.values()) { val generatedStringsFromBaseRuleset = @@ -250,7 +250,7 @@ object TranslationFileWriter { val array = jsonParser.getFromJson(javaClass, jsonFile.path()) generatedStrings[filename] = mutableSetOf() - val resultStrings = generatedStrings[filename] + val resultStrings = generatedStrings[filename]!! fun submitString(item: Any) { val string = item.toString() @@ -285,8 +285,15 @@ object TranslationFileWriter { stringToTranslate = stringToTranslate.replaceFirst(parameter, parameterName) } + } else if (string.contains('{')) { + val matches = curlyBraceRegex.findAll(string) + if (matches.any()) { + // Ignore outer string, only translate the parts within `{}` + matches.forEach { submitString(it.groups[1]!!.value) } + return + } } - resultStrings!!.add("$stringToTranslate = ") + resultStrings.add("$stringToTranslate = ") return } @@ -311,7 +318,7 @@ object TranslationFileWriter { for (field in allFields) { field.isAccessible = true val fieldValue = field.get(element) - if (isFieldTranslatable(field, fieldValue)) { // skip fields which must not be translated + if (isFieldTranslatable(javaClass, field, fieldValue)) { // skip fields which must not be translated // this field can contain sub-objects, let's serialize them as well @Suppress("RemoveRedundantQualifierName") // to clarify List does _not_ inherit from anything in java.util when (fieldValue) { @@ -331,7 +338,7 @@ object TranslationFileWriter { for (element in array) { serializeElement(element!!) // let's serialize the strings recursively // This is a small hack to insert multiple /n into the set, which can't contain identical lines - resultStrings!!.add("$specialNewLineCode ${uniqueIndexOfNewLine++}") + resultStrings.add("$specialNewLineCode ${uniqueIndexOfNewLine++}") } } println("Translation writer took ${System.currentTimeMillis()-startMillis}ms for ${jsonsFolder.name()}") @@ -339,6 +346,12 @@ object TranslationFileWriter { return generatedStrings } + /** Exclude fields by name that contain references to items defined elsewhere + * or are otherwise Strings but not user-displayed. + * + * An exclusion applies either over _all_ json files and all classes contained in them + * or Class-specific by using a "Class.Field" notation. + */ private val untranslatableFieldSet = setOf( "aiFreeTechs", "aiFreeUnits", "attackSound", "building", "cannotBeBuiltWith", "cultureBuildings", "improvement", "improvingTech", @@ -347,21 +360,24 @@ object TranslationFileWriter { "requiredNearbyImprovedResources", "requiredResource", "requiredTech", "requires", "resourceTerrainAllow", "revealedBy", "startBias", "techRequired", "terrainsCanBeBuiltOn", "terrainsCanBeFoundOn", "turnsInto", "uniqueTo", "upgradesTo", - "link", "icon", "extraImage", "color" // FormattedLine + "link", "icon", "extraImage", "color", // FormattedLine + "excludedDifficulties", "RuinReward.uniques" // RuinReward ) + /** Specifies Enums where the name property _is_ translatable, by Class name */ private val translatableEnumsSet = setOf("BeliefType") - private fun isFieldTranslatable(field: Field, fieldValue: Any?): Boolean { - // Exclude fields by name that contain references to items defined elsewhere - // or are otherwise Strings but not user-displayed. - // The Modifier.STATIC exclusion removes fields from e.g. companion objects. - // Fields of enum types need that type explicitly allowed in translatableEnumsSet - // (Using an annotation failed, even with @Retention RUNTIME they were invisible to reflection) + /** Checks whether a field's value should be included in the translation templates. + * Applies explicit field exclusions from [untranslatableFieldSet]. + * The Modifier.STATIC exclusion removes fields from e.g. companion objects. + * Fields of enum types need that type explicitly allowed in [translatableEnumsSet] + */ + private fun isFieldTranslatable(clazz: Class<*>, field: Field, fieldValue: Any?): Boolean { return fieldValue != null && fieldValue != "" && (field.modifiers and Modifier.STATIC) == 0 && (!field.type.isEnum || field.type.simpleName in translatableEnumsSet) && - field.name !in untranslatableFieldSet + field.name !in untranslatableFieldSet && + (clazz.componentType?.simpleName ?: clazz.simpleName) + "." + field.name !in untranslatableFieldSet } private fun getJavaClassByName(name: String): Class { @@ -374,6 +390,7 @@ object TranslationFileWriter { "Policies" -> emptyArray().javaClass "Quests" -> emptyArray().javaClass "Religions" -> emptyArray().javaClass + "Ruins" -> emptyArray().javaClass "Specialists" -> emptyArray().javaClass "Techs" -> emptyArray().javaClass "Terrains" -> emptyArray().javaClass diff --git a/core/src/com/unciv/ui/civilopedia/CivilopediaScreen.kt b/core/src/com/unciv/ui/civilopedia/CivilopediaScreen.kt index 9a2e312d36..ec0c100ca5 100644 --- a/core/src/com/unciv/ui/civilopedia/CivilopediaScreen.kt +++ b/core/src/com/unciv/ui/civilopedia/CivilopediaScreen.kt @@ -379,8 +379,8 @@ class CivilopediaScreen( val hideReligionItems = !game.gameInfo.hasReligionEnabled() val noCulturalVictory = VictoryType.Cultural !in game.gameInfo.gameParameters.victoryTypes - return "Will not be displayed in Civilopedia" !in uniques - && !(hideReligionItems && "Hidden when religion is disabled" in uniques) + return Constants.hideFromCivilopediaUnique !in uniques + && !(hideReligionItems && Constants.hiddenWithoutReligionUnique in uniques) && !(uniqueObjects.filter { unique -> unique.placeholderText == "Hidden when [] Victory is disabled"}.any { unique -> !game.gameInfo.gameParameters.victoryTypes.contains(VictoryType.valueOf(unique.params[0] )) })