From 3ea1e4a539d2fa66578a76418e83285659bc7eb1 Mon Sep 17 00:00:00 2001 From: SeventhM <127357473+SeventhM@users.noreply.github.com> Date: Thu, 4 Apr 2024 13:39:44 -0700 Subject: [PATCH] Allow for replacement improvements (#11369) * Allow for replacement improvements * imports * Forgot the most important change, lol * Docs * Replacement description, validation, and filter * Move more into ImprovementDescriptions * Whoops, forgot to yield * Fix some copy-paste artifacts * New translations * Fix double see also * Add space for translation engine --- .../jsons/translations/template.properties | 2 + .../com/unciv/logic/automation/Automation.kt | 2 +- .../com/unciv/logic/city/CityConstructions.kt | 10 +- .../unciv/logic/civilization/Civilization.kt | 17 ++ .../transients/CivInfoTransientCache.kt | 8 + .../map/tile/TileInfoImprovementFunctions.kt | 3 + core/src/com/unciv/models/ruleset/Building.kt | 4 + .../com/unciv/models/ruleset/nation/Nation.kt | 21 +- .../models/ruleset/tile/TileImprovement.kt | 127 +---------- .../ruleset/validation/RulesetValidator.kt | 4 +- .../ImprovementDescriptions.kt | 197 ++++++++++++++++++ .../cityscreen/CityConstructionsTable.kt | 3 +- .../unciv/ui/screens/cityscreen/CityScreen.kt | 4 +- .../3-Map-related-JSON-files.md | 1 + 14 files changed, 264 insertions(+), 139 deletions(-) create mode 100644 core/src/com/unciv/ui/objectdescriptions/ImprovementDescriptions.kt diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index efcb42fffb..50f2ea62f5 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -1538,6 +1538,7 @@ Unique to [civName], replaces [unitName] = Unique to [civName] = Tutorials = Cost = +Turns to build = May contain [listOfResources] = May contain: = Can upgrade from [unit] = @@ -1557,6 +1558,7 @@ Improvements that provide this resource = Buildings that require this resource worked near the city = Units that consume this resource = Can be built on = +Cannot be built on = or [terrainType] = Can be constructed by = Can be created instantly by = diff --git a/core/src/com/unciv/logic/automation/Automation.kt b/core/src/com/unciv/logic/automation/Automation.kt index ff0c21a98f..1f40470437 100644 --- a/core/src/com/unciv/logic/automation/Automation.kt +++ b/core/src/com/unciv/logic/automation/Automation.kt @@ -275,7 +275,7 @@ object Automation { ): Boolean { if (construction !is Building) return true if (!construction.hasCreateOneImprovementUnique()) return true // redundant but faster??? - val improvement = construction.getImprovementToCreate(city.getRuleset()) ?: return true + val improvement = construction.getImprovementToCreate(city.getRuleset(), civInfo) ?: return true return city.getTiles().any { it.improvementFunctions.canBuildImprovement(improvement, civInfo) } diff --git a/core/src/com/unciv/logic/city/CityConstructions.kt b/core/src/com/unciv/logic/city/CityConstructions.kt index 314a700a6f..d10064919a 100644 --- a/core/src/com/unciv/logic/city/CityConstructions.kt +++ b/core/src/com/unciv/logic/city/CityConstructions.kt @@ -655,7 +655,7 @@ class CityConstructions : IsPartOfGameInfoSerialization { tile: Tile? = null ): Boolean { // Support UniqueType.CreatesOneImprovement: it is active when getImprovementToCreate returns an improvement - val improvementToPlace = (construction as? Building)?.getImprovementToCreate(city.getRuleset()) + val improvementToPlace = (construction as? Building)?.getImprovementToCreate(city.getRuleset(), city.civ) if (improvementToPlace != null) { // If active without a predetermined tile to place the improvement on, automate a tile val finalTile = tile @@ -716,7 +716,7 @@ class CityConstructions : IsPartOfGameInfoSerialization { /** Support for [UniqueType.CreatesOneImprovement] - if an Improvement-creating Building was auto-queued, auto-choose a tile: */ val building = getCurrentConstruction() as? Building ?: return - val improvement = building.getImprovementToCreate(city.getRuleset()) ?: return + val improvement = building.getImprovementToCreate(city.getRuleset(), city.civ) ?: return if (getTileForImprovement(improvement.name) != null) return val newTile = Automation.getTileForConstructionImprovement(city, improvement) ?: return newTile.improvementFunctions.markForCreatesOneImprovement(improvement.name) @@ -778,7 +778,7 @@ class CityConstructions : IsPartOfGameInfoSerialization { // UniqueType.CreatesOneImprovement support val construction = getConstruction(constructionName) if (construction is Building) { - val improvement = construction.getImprovementToCreate(city.getRuleset()) + val improvement = construction.getImprovementToCreate(city.getRuleset(), city.civ) if (improvement != null) { getTileForImprovement(improvement.name)?.stopWorkingOnImprovement() } @@ -846,7 +846,7 @@ class CityConstructions : IsPartOfGameInfoSerialization { * (skip if none found), then un-mark the tile and place the improvement unless [removeOnly] is set. */ private fun applyCreateOneImprovement(building: Building, removeOnly: Boolean = false) { - val improvement = building.getImprovementToCreate(city.getRuleset()) + val improvement = building.getImprovementToCreate(city.getRuleset(), city.civ) ?: return val tileForImprovement = getTileForImprovement(improvement.name) ?: return tileForImprovement.stopWorkingOnImprovement() // clears mark @@ -866,7 +866,7 @@ class CityConstructions : IsPartOfGameInfoSerialization { val ruleset = city.getRuleset() val indexToRemove = constructionQueue.withIndex().firstNotNullOfOrNull { val construction = getConstruction(it.value) - val buildingImprovement = (construction as? Building)?.getImprovementToCreate(ruleset)?.name + val buildingImprovement = (construction as? Building)?.getImprovementToCreate(ruleset, city.civ)?.name it.index.takeIf { buildingImprovement == improvement } } ?: return diff --git a/core/src/com/unciv/logic/civilization/Civilization.kt b/core/src/com/unciv/logic/civilization/Civilization.kt index 060f8c253e..ee29c7a1bd 100644 --- a/core/src/com/unciv/logic/civilization/Civilization.kt +++ b/core/src/com/unciv/logic/civilization/Civilization.kt @@ -45,6 +45,7 @@ import com.unciv.models.ruleset.nation.Personality import com.unciv.models.ruleset.tech.Era import com.unciv.models.ruleset.tile.ResourceSupplyList import com.unciv.models.ruleset.tile.ResourceType +import com.unciv.models.ruleset.tile.TileImprovement import com.unciv.models.ruleset.tile.TileResource import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.TemporaryUnique @@ -544,6 +545,22 @@ class Civilization : IsPartOfGameInfoSerialization { return baseBuilding } + fun getEquivalentTileImprovement(tileImprovementName: String): TileImprovement { + val tileImprovement = gameInfo.ruleset.tileImprovements[tileImprovementName] + ?: throw UncivShowableException("Improvement $tileImprovementName doesn't seem to exist!") + return getEquivalentTileImprovement(tileImprovement) + } + + fun getEquivalentTileImprovement(tileImprovement: TileImprovement): TileImprovement { + if (tileImprovement.replaces != null) + return getEquivalentTileImprovement(tileImprovement.replaces!!) + + for (improvement in cache.uniqueImprovements) + if (improvement.replaces == tileImprovement.name) + return improvement + return tileImprovement + } + fun getEquivalentUnit(baseUnitName: String): BaseUnit { val baseUnit = gameInfo.ruleset.units[baseUnitName] ?: throw UncivShowableException("Unit $baseUnitName doesn't seem to exist!") diff --git a/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt b/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt index 8e1b9673c4..5548a2b664 100644 --- a/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt +++ b/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt @@ -14,6 +14,7 @@ import com.unciv.logic.map.tile.Tile import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.tile.ResourceSupplyList import com.unciv.models.ruleset.tile.ResourceType +import com.unciv.models.ruleset.tile.TileImprovement import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.models.ruleset.unique.UniqueTriggerActivation @@ -35,6 +36,9 @@ class CivInfoTransientCache(val civInfo: Civilization) { @Transient val uniqueUnits = hashSetOf() + @Transient + val uniqueImprovements = hashSetOf() + @Transient val uniqueBuildings = hashSetOf() @@ -64,6 +68,10 @@ class CivInfoTransientCache(val civInfo: Civilization) { } } + for (improvement in ruleset.tileImprovements.values) + if (improvement.uniqueTo == civInfo.civName) + uniqueImprovements.add(improvement) + for (unit in ruleset.units.values) { if (unit.uniqueTo == civInfo.civName) { uniqueUnits.add(unit) diff --git a/core/src/com/unciv/logic/map/tile/TileInfoImprovementFunctions.kt b/core/src/com/unciv/logic/map/tile/TileInfoImprovementFunctions.kt index 3abb2ca5a3..9a49b0e473 100644 --- a/core/src/com/unciv/logic/map/tile/TileInfoImprovementFunctions.kt +++ b/core/src/com/unciv/logic/map/tile/TileInfoImprovementFunctions.kt @@ -20,6 +20,7 @@ enum class ImprovementBuildingProblem( /** `true` if the ImprovementPicker should report this problem */ val reportable: Boolean = false ) { + Replaced(permanent = true), WrongCiv(permanent = true), MissingTech(reportable = true), Unbuildable(permanent = true), @@ -45,6 +46,8 @@ class TileInfoImprovementFunctions(val tile: Tile) { if (improvement.uniqueTo != null && improvement.uniqueTo != civInfo.civName) yield(ImprovementBuildingProblem.WrongCiv) + if (civInfo.cache.uniqueImprovements.any { it.replaces == improvement.name }) + yield(ImprovementBuildingProblem.Replaced) if (improvement.techRequired != null && !civInfo.tech.isResearched(improvement.techRequired!!)) yield(ImprovementBuildingProblem.MissingTech) if (improvement.getMatchingUniques(UniqueType.Unbuildable, StateForConditionals.IgnoreConditionals) diff --git a/core/src/com/unciv/models/ruleset/Building.kt b/core/src/com/unciv/models/ruleset/Building.kt index ce956e5c3e..d6eb2e307f 100644 --- a/core/src/com/unciv/models/ruleset/Building.kt +++ b/core/src/com/unciv/models/ruleset/Building.kt @@ -551,6 +551,10 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction { } return _getImprovementToCreate } + fun getImprovementToCreate(ruleset: Ruleset, civInfo: Civilization): TileImprovement? { + val improvement = getImprovementToCreate(ruleset) ?: return null + return civInfo.getEquivalentTileImprovement(improvement) + } fun isSellable() = !isAnyWonder() && !hasUnique(UniqueType.Unsellable) diff --git a/core/src/com/unciv/models/ruleset/nation/Nation.kt b/core/src/com/unciv/models/ruleset/nation/Nation.kt index 23110bc219..34215e458c 100644 --- a/core/src/com/unciv/models/ruleset/nation/Nation.kt +++ b/core/src/com/unciv/models/ruleset/nation/Nation.kt @@ -13,6 +13,7 @@ import com.unciv.models.translations.tr import com.unciv.ui.components.extensions.colorFromRGB import com.unciv.ui.objectdescriptions.BaseUnitDescriptions import com.unciv.ui.objectdescriptions.BuildingDescriptions +import com.unciv.ui.objectdescriptions.ImprovementDescriptions import com.unciv.ui.objectdescriptions.uniquesToCivilopediaTextLines import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen.Companion.showReligionInCivilopedia import com.unciv.ui.screens.civilopediascreen.FormattedLine @@ -246,17 +247,15 @@ class Nation : RulesetObject() { yield(FormattedLine(separator = true)) yield(FormattedLine(improvement.name, link = "Improvement/${improvement.name}")) yield(FormattedLine(improvement.cloneStats().toString(), indent = 1)) // = (improvement as Stats).toString minus import plus copy overhead - if (improvement.terrainsCanBeBuiltOn.isNotEmpty()) { - improvement.terrainsCanBeBuiltOn.withIndex().forEach { - yield( - FormattedLine(if (it.index == 0) "{Can be built on} {${it.value}}" else "or [${it.value}]", - link = "Terrain/${it.value}", indent = if (it.index == 0) 1 else 2) - ) - } - } - for (unique in improvement.uniqueObjects) { - if (unique.isHiddenToUsers()) continue - yield(FormattedLine(unique, indent = 1)) + if (improvement.replaces != null && ruleset.tileImprovements.containsKey(improvement.replaces!!)) { + val originalImprovement = ruleset.tileImprovements[improvement.replaces!!]!! + yield(FormattedLine("Replaces [${originalImprovement.name}]", link = originalImprovement.makeLink(), indent=1)) + yieldAll(ImprovementDescriptions.getDifferences(ruleset, originalImprovement, improvement)) + yield(FormattedLine()) + } else if (improvement.replaces != null) { + yield(FormattedLine("Replaces [${improvement.replaces}], which is not found in the ruleset!", indent=1)) + } else { + yieldAll(improvement.getShortDecription()) } } } diff --git a/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt b/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt index b4a7f07443..bb57e978f0 100644 --- a/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt +++ b/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt @@ -1,28 +1,24 @@ package com.unciv.models.ruleset.tile import com.unciv.Constants -import com.unciv.UncivGame import com.unciv.logic.MultiFilter import com.unciv.logic.civilization.Civilization import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.tile.RoadStatus -import com.unciv.models.ruleset.Belief import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetStatsObject import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unit.BaseUnit -import com.unciv.models.translations.tr import com.unciv.ui.components.extensions.toPercent -import com.unciv.ui.objectdescriptions.uniquesToCivilopediaTextLines -import com.unciv.ui.objectdescriptions.uniquesToDescription -import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen.Companion.showReligionInCivilopedia +import com.unciv.ui.objectdescriptions.ImprovementDescriptions import com.unciv.ui.screens.civilopediascreen.FormattedLine import kotlin.math.roundToInt class TileImprovement : RulesetStatsObject() { + var replaces: String? = null var terrainsCanBeBuiltOn: Collection = ArrayList() var techRequired: String? = null var uniqueTo: String? = null @@ -44,29 +40,8 @@ class TileImprovement : RulesetStatsObject() { // In some weird cases it was possible for something to take 0 turns, leading to it instead never finishing } - fun getDescription(ruleset: Ruleset): String { - val lines = ArrayList() - - val statsDesc = cloneStats().toString() - if (statsDesc.isNotEmpty()) lines += statsDesc - if (!terrainsCanBeBuiltOn.isEmpty()) { - val terrainsCanBeBuiltOnString: ArrayList = arrayListOf() - for (i in terrainsCanBeBuiltOn) { - terrainsCanBeBuiltOnString.add(i.tr()) - } - lines += "Can be built on".tr() + terrainsCanBeBuiltOnString.joinToString(", ", " ") //language can be changed when setting changes. - } - for (resource: TileResource in ruleset.tileResources.values.filter { it.isImprovedBy(name) }) { - if (resource.improvementStats == null) continue - val statsString = resource.improvementStats.toString() - lines += "[${statsString}] ".tr() - } - if (techRequired != null) lines += "Required tech: [$techRequired]".tr() - - uniquesToDescription(lines) - - return lines.joinToString("\n") - } + fun getDescription(ruleset: Ruleset): String = ImprovementDescriptions.getDescription(this, ruleset) + fun getShortDecription() = ImprovementDescriptions.getShortDescription(this) fun isGreatImprovement() = hasUnique(UniqueType.GreatImprovement) fun isRoad() = RoadStatus.values().any { it != RoadStatus.None && it.name == this.name } @@ -100,6 +75,7 @@ class TileImprovement : RulesetStatsObject() { private fun matchesSingleFilter(filter: String): Boolean { return when (filter) { name -> true + replaces -> true in Constants.all -> true "Improvement" -> true // For situations involving tileFilter "All Road" -> isRoad() @@ -111,95 +87,10 @@ class TileImprovement : RulesetStatsObject() { override fun makeLink() = "Improvement/$name" - override fun getCivilopediaTextLines(ruleset: Ruleset): List { - val textList = ArrayList() + override fun getCivilopediaTextLines(ruleset: Ruleset): List = + ImprovementDescriptions.getCivilopediaTextLines(this, ruleset) - val statsDesc = cloneStats().toString() - if (statsDesc.isNotEmpty()) textList += FormattedLine(statsDesc) - - if (uniqueTo != null) { - textList += FormattedLine() - textList += FormattedLine("Unique to [$uniqueTo]", link="Nation/$uniqueTo") - } - - val constructorUnits = getConstructorUnits(ruleset) - val creatingUnits = getCreatingUnits(ruleset) - val creatorExists = constructorUnits.isNotEmpty() || creatingUnits.isNotEmpty() - - if (creatorExists && terrainsCanBeBuiltOn.isNotEmpty()) { - textList += FormattedLine() - if (terrainsCanBeBuiltOn.size == 1) { - with (terrainsCanBeBuiltOn.first()) { - textList += FormattedLine("{Can be built on} {$this}", link="Terrain/$this") - } - } else { - textList += FormattedLine("{Can be built on}:") - terrainsCanBeBuiltOn.forEach { - textList += FormattedLine(it, link="Terrain/$it", indent=1) - } - } - } - - var addedLineBeforeResourceBonus = false - for (resource in ruleset.tileResources.values) { - if (resource.improvementStats == null || !resource.isImprovedBy(name)) continue - if (!addedLineBeforeResourceBonus) { - addedLineBeforeResourceBonus = true - textList += FormattedLine() - } - val statsString = resource.improvementStats.toString() - // Line intentionally modeled as UniqueType.Stats + ConditionalInTiles - textList += FormattedLine("[${statsString}] ", link = resource.makeLink()) - } - - if (techRequired != null) { - textList += FormattedLine() - textList += FormattedLine("Required tech: [$techRequired]", link="Technology/$techRequired") - } - - uniquesToCivilopediaTextLines(textList) - - // Be clearer when one needs to chop down a Forest first... A "Can be built on Plains" is clear enough, - // but a "Can be built on Land" is not - how is the user to know Forest is _not_ Land? - if (creatorExists && - !isEmpty() && // Has any Stats - !hasUnique(UniqueType.NoFeatureRemovalNeeded) && - !hasUnique(UniqueType.RemovesFeaturesIfBuilt) && - terrainsCanBeBuiltOn.none { it in ruleset.terrains } - ) - textList += FormattedLine("Needs removal of terrain features to be built") - - if (isAncientRuinsEquivalent() && ruleset.ruinRewards.isNotEmpty()) { - val difficulty = if (!UncivGame.isCurrentInitialized() || UncivGame.Current.gameInfo == null) - "Prince" // most factors == 1 - else UncivGame.Current.gameInfo!!.gameParameters.difficulty - val religionEnabled = showReligionInCivilopedia(ruleset) - textList += FormattedLine() - textList += FormattedLine("The possible rewards are:") - ruleset.ruinRewards.values.asSequence() - .filter { reward -> - difficulty !in reward.excludedDifficulties && - (religionEnabled || !reward.hasUnique(UniqueType.HiddenWithoutReligion)) - } - .forEach { reward -> - textList += FormattedLine(reward.name, starred = true, color = reward.color) - textList += reward.civilopediaText - } - } - - if (creatorExists) - textList += FormattedLine() - for (unit in constructorUnits) - textList += FormattedLine("{Can be constructed by} {$unit}", unit.makeLink()) - for (unit in creatingUnits) - textList += FormattedLine("{Can be created instantly by} {$unit}", unit.makeLink()) - - textList += Belief.getCivilopediaTextMatching(name, ruleset) - - return textList - } - - private fun getConstructorUnits(ruleset: Ruleset): List { + fun getConstructorUnits(ruleset: Ruleset): List { //todo Why does this have to be so complicated? A unit's "Can build [Land] improvements on tiles" // creates the _justified_ expectation that an improvement it can build _will_ have // `matchesFilter("Land")==true` - but that's not the case. @@ -251,7 +142,7 @@ class TileImprovement : RulesetStatsObject() { }.toList() } - private fun getCreatingUnits(ruleset: Ruleset): List { + fun getCreatingUnits(ruleset: Ruleset): List { return ruleset.units.values.asSequence() .filter { unit -> unit.getMatchingUniques(UniqueType.ConstructImprovementInstantly, StateForConditionals.IgnoreConditionals) diff --git a/core/src/com/unciv/models/ruleset/validation/RulesetValidator.kt b/core/src/com/unciv/models/ruleset/validation/RulesetValidator.kt index 3a30e3f99a..35718972d1 100644 --- a/core/src/com/unciv/models/ruleset/validation/RulesetValidator.kt +++ b/core/src/com/unciv/models/ruleset/validation/RulesetValidator.kt @@ -401,13 +401,15 @@ class RulesetValidator(val ruleset: Ruleset) { for (improvement in ruleset.tileImprovements.values) { if (improvement.techRequired != null && !ruleset.technologies.containsKey(improvement.techRequired!!)) lines.add("${improvement.name} requires tech ${improvement.techRequired} which does not exist!", sourceObject = improvement) + if (improvement.replaces != null && !ruleset.tileImprovements.containsKey(improvement.replaces)) + lines.add("${improvement.name} replaces ${improvement.replaces} which does not exist!", sourceObject = improvement) for (terrain in improvement.terrainsCanBeBuiltOn) if (!ruleset.terrains.containsKey(terrain) && terrain != "Land" && terrain != "Water") lines.add("${improvement.name} can be built on terrain $terrain which does not exist!", sourceObject = improvement) if (improvement.terrainsCanBeBuiltOn.isEmpty() && !improvement.hasUnique(UniqueType.CanOnlyImproveResource) && !improvement.hasUnique(UniqueType.Unbuildable) - && !improvement.name.startsWith(Constants.remove) + && improvement !in ruleset.tileRemovals && improvement.name !in RoadStatus.values().map { it.removeAction } && improvement.name != Constants.cancelImprovementOrder ) { diff --git a/core/src/com/unciv/ui/objectdescriptions/ImprovementDescriptions.kt b/core/src/com/unciv/ui/objectdescriptions/ImprovementDescriptions.kt new file mode 100644 index 0000000000..8dd60047c7 --- /dev/null +++ b/core/src/com/unciv/ui/objectdescriptions/ImprovementDescriptions.kt @@ -0,0 +1,197 @@ +package com.unciv.ui.objectdescriptions + +import com.unciv.UncivGame +import com.unciv.models.ruleset.Belief +import com.unciv.models.ruleset.Ruleset +import com.unciv.models.ruleset.tile.TileImprovement +import com.unciv.models.ruleset.tile.TileResource +import com.unciv.models.ruleset.unique.Unique +import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.models.translations.tr +import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen +import com.unciv.ui.screens.civilopediascreen.FormattedLine + +object ImprovementDescriptions { + /** + * Lists differences: how a nation-unique Improvement compares to its replacement. + * + * Result as indented, non-linking [FormattedLine]s + * + * @param originalImprovement The "standard" Improvement + * @param replacementImprovement The "uniqueTo" Improvement + */ + fun getDifferences( + ruleset: Ruleset, originalImprovement: TileImprovement, replacementImprovement: TileImprovement + ): Sequence = sequence { + for ((key, value) in replacementImprovement) + if (value != originalImprovement[key]) + yield(FormattedLine( key.name.tr() + " " +"[${value.toInt()}] vs [${originalImprovement[key].toInt()}]".tr(), indent=1)) + + for (terrain in replacementImprovement.terrainsCanBeBuiltOn) + if (terrain !in originalImprovement.terrainsCanBeBuiltOn) + yield(FormattedLine("Can be built on [${terrain}]", link = ruleset.terrains[terrain]?.makeLink() ?: "", indent = 1)) + for (terrain in originalImprovement.terrainsCanBeBuiltOn) + if (terrain !in replacementImprovement.terrainsCanBeBuiltOn) + yield(FormattedLine("Cannot be built on [${terrain}]", link = ruleset.terrains[terrain]?.makeLink() ?: "", indent = 1)) + + if (replacementImprovement.turnsToBuild != originalImprovement.turnsToBuild) + yield(FormattedLine("{Turns to build} ".tr() + "[${replacementImprovement.turnsToBuild}] vs [${originalImprovement.turnsToBuild}]".tr(), indent=1)) + + val newAbilityPredicate: (Unique)->Boolean = { it.text in originalImprovement.uniques || it.isHiddenToUsers() } + for (unique in replacementImprovement.uniqueObjects.filterNot(newAbilityPredicate)) + yield(FormattedLine(unique.text, indent=1)) // FormattedLine(unique) would look worse - no indent and autolinking could distract + + val lostAbilityPredicate: (Unique)->Boolean = { it.text in replacementImprovement.uniques || it.isHiddenToUsers() } + for (unique in originalImprovement.uniqueObjects.filterNot(lostAbilityPredicate)) { + // Need double translation of the "ability" here - unique texts may contain square brackets + yield(FormattedLine("Lost ability (vs [${originalImprovement.name}]): [${unique.text.tr()}]", indent=1)) + } + } + + fun getCivilopediaTextLines(improvement: TileImprovement, ruleset: Ruleset): List { + val textList = ArrayList() + + val statsDesc = improvement.cloneStats().toString() + if (statsDesc.isNotEmpty()) textList += FormattedLine(statsDesc) + + if (improvement.uniqueTo != null) { + textList += FormattedLine() + textList += FormattedLine("Unique to [${improvement.uniqueTo}]", link="Nation/${improvement.uniqueTo}") + } + if (improvement.replaces != null) { + val replaceImprovement = ruleset.tileImprovements[improvement.replaces] + textList += FormattedLine("Replaces [${improvement.replaces}]", link=replaceImprovement?.makeLink() ?: "", indent = 1) + } + + val constructorUnits = improvement.getConstructorUnits(ruleset) + val creatingUnits = improvement.getCreatingUnits(ruleset) + val creatorExists = constructorUnits.isNotEmpty() || creatingUnits.isNotEmpty() + + if (creatorExists && improvement.terrainsCanBeBuiltOn.isNotEmpty()) { + textList += FormattedLine() + if (improvement.terrainsCanBeBuiltOn.size == 1) { + with (improvement.terrainsCanBeBuiltOn.first()) { + textList += FormattedLine("{Can be built on} {$this}", link="Terrain/$this") + } + } else { + textList += FormattedLine("{Can be built on}:") + improvement.terrainsCanBeBuiltOn.forEach { + textList += FormattedLine(it, link="Terrain/$it", indent=1) + } + } + } + + var addedLineBeforeResourceBonus = false + for (resource in ruleset.tileResources.values) { + if (resource.improvementStats == null || !resource.isImprovedBy(improvement.name)) continue + if (!addedLineBeforeResourceBonus) { + addedLineBeforeResourceBonus = true + textList += FormattedLine() + } + val statsString = resource.improvementStats.toString() + // Line intentionally modeled as UniqueType.Stats + ConditionalInTiles + textList += FormattedLine("[${statsString}] ", link = resource.makeLink()) + } + + if (improvement.techRequired != null) { + textList += FormattedLine() + textList += FormattedLine("Required tech: [${improvement.techRequired}]", link="Technology/${improvement.techRequired}") + } + + improvement.uniquesToCivilopediaTextLines(textList) + + // Be clearer when one needs to chop down a Forest first... A "Can be built on Plains" is clear enough, + // but a "Can be built on Land" is not - how is the user to know Forest is _not_ Land? + if (creatorExists && + !improvement.isEmpty() && // Has any Stats + !improvement.hasUnique(UniqueType.NoFeatureRemovalNeeded) && + !improvement.hasUnique(UniqueType.RemovesFeaturesIfBuilt) && + improvement.terrainsCanBeBuiltOn.none { it in ruleset.terrains } + ) + textList += FormattedLine("Needs removal of terrain features to be built") + + if (improvement.isAncientRuinsEquivalent() && ruleset.ruinRewards.isNotEmpty()) { + val difficulty = if (!UncivGame.isCurrentInitialized() || UncivGame.Current.gameInfo == null) + "Prince" // most factors == 1 + else UncivGame.Current.gameInfo!!.gameParameters.difficulty + val religionEnabled = CivilopediaScreen.showReligionInCivilopedia(ruleset) + textList += FormattedLine() + textList += FormattedLine("The possible rewards are:") + ruleset.ruinRewards.values.asSequence() + .filter { reward -> + difficulty !in reward.excludedDifficulties && + (religionEnabled || !reward.hasUnique(UniqueType.HiddenWithoutReligion)) + } + .forEach { reward -> + textList += FormattedLine(reward.name, starred = true, color = reward.color) + textList += reward.civilopediaText + } + } + + if (creatorExists) + textList += FormattedLine() + for (unit in constructorUnits) + textList += FormattedLine("{Can be constructed by} {$unit}", unit.makeLink()) + for (unit in creatingUnits) + textList += FormattedLine("{Can be created instantly by} {$unit}", unit.makeLink()) + + val seeAlso = ArrayList() + for (alsoImprovement in ruleset.tileImprovements.values) { + if (alsoImprovement.replaces == improvement.name) + seeAlso += FormattedLine(alsoImprovement.name, link = alsoImprovement.makeLink(), indent = 1) + } + + seeAlso += Belief.getCivilopediaTextMatching(improvement.name, ruleset, false) + + if (seeAlso.isNotEmpty()) { + textList += FormattedLine() + textList += FormattedLine("{See also}:") + textList += seeAlso + } + + return textList + } + + fun getDescription(improvement: TileImprovement, ruleset: Ruleset): String { + val lines = ArrayList() + + val statsDesc = improvement.cloneStats().toString() + if (statsDesc.isNotEmpty()) lines += statsDesc + if (improvement.uniqueTo != null) lines += "Unique to [${improvement.uniqueTo}]".tr() + if (improvement.replaces != null) lines += "Replaces [${improvement.replaces}]".tr() + if (!improvement.terrainsCanBeBuiltOn.isEmpty()) { + val terrainsCanBeBuiltOnString: ArrayList = arrayListOf() + for (i in improvement.terrainsCanBeBuiltOn) { + terrainsCanBeBuiltOnString.add(i.tr()) + } + lines += "Can be built on".tr() + terrainsCanBeBuiltOnString.joinToString(", ", " ") //language can be changed when setting changes. + } + for (resource: TileResource in ruleset.tileResources.values.filter { it.isImprovedBy(improvement.name) }) { + if (resource.improvementStats == null) continue + val statsString = resource.improvementStats.toString() + lines += "[${statsString}] ".tr() + } + if (improvement.techRequired != null) lines += "Required tech: [${improvement.techRequired}]".tr() + + improvement.uniquesToDescription(lines) + + return lines.joinToString("\n") + } + + fun getShortDescription(improvement: TileImprovement) = sequence { + if (improvement.terrainsCanBeBuiltOn.isNotEmpty()) { + improvement.terrainsCanBeBuiltOn.withIndex().forEach { + yield( + FormattedLine( + if (it.index == 0) "{Can be built on} {${it.value}}" else "or [${it.value}]", + link = "Terrain/${it.value}", indent = if (it.index == 0) 1 else 2 + ) + ) + } + } + for (unique in improvement.uniqueObjects) { + if (unique.isHiddenToUsers()) continue + yield(FormattedLine(unique, indent = 1)) + } + } +} diff --git a/core/src/com/unciv/ui/screens/cityscreen/CityConstructionsTable.kt b/core/src/com/unciv/ui/screens/cityscreen/CityConstructionsTable.kt index 6451b32af8..209f9fb257 100644 --- a/core/src/com/unciv/ui/screens/cityscreen/CityConstructionsTable.kt +++ b/core/src/com/unciv/ui/screens/cityscreen/CityConstructionsTable.kt @@ -660,7 +660,8 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { return cityScreen.startPickTileForCreatesOneImprovement(construction, stat, true) // Buying a UniqueType.CreatesOneImprovement building from queue must pass down // the already selected tile, otherwise a new one is chosen from Automation code. - val improvement = construction.getImprovementToCreate(cityScreen.city.getRuleset())!! + val improvement = construction.getImprovementToCreate( + cityScreen.city.getRuleset(), cityScreen.city.civ)!! val tileForImprovement = cityScreen.city.cityConstructions.getTileForImprovement(improvement.name) askToBuyConstruction(construction, stat, tileForImprovement) } diff --git a/core/src/com/unciv/ui/screens/cityscreen/CityScreen.kt b/core/src/com/unciv/ui/screens/cityscreen/CityScreen.kt index d20cdbda2f..311c62079f 100644 --- a/core/src/com/unciv/ui/screens/cityscreen/CityScreen.kt +++ b/core/src/com/unciv/ui/screens/cityscreen/CityScreen.kt @@ -454,7 +454,7 @@ class CityScreen( fun selectConstruction(newConstruction: IConstruction) { selectedConstruction = newConstruction if (newConstruction is Building && newConstruction.hasCreateOneImprovementUnique()) { - val improvement = newConstruction.getImprovementToCreate(city.getRuleset()) + val improvement = newConstruction.getImprovementToCreate(city.getRuleset(), city.civ) selectedQueueEntryTargetTile = if (improvement == null) null else city.cityConstructions.getTileForImprovement(improvement.name) } else { @@ -472,7 +472,7 @@ class CityScreen( fun clearSelection() = selectTile(null) fun startPickTileForCreatesOneImprovement(construction: Building, stat: Stat, isBuying: Boolean) { - val improvement = construction.getImprovementToCreate(city.getRuleset()) ?: return + val improvement = construction.getImprovementToCreate(city.getRuleset(), city.civ) ?: return pickTileData = PickTileForImprovementData(construction, improvement, isBuying, stat) updateTileGroups() ToastPopup("Please select a tile for this building's [${improvement.name}]", this) diff --git a/docs/Modders/Mod-file-structure/3-Map-related-JSON-files.md b/docs/Modders/Mod-file-structure/3-Map-related-JSON-files.md index e91605c36e..c4736b2b7a 100644 --- a/docs/Modders/Mod-file-structure/3-Map-related-JSON-files.md +++ b/docs/Modders/Mod-file-structure/3-Map-related-JSON-files.md @@ -45,6 +45,7 @@ Each improvement has the following structure: | name | String | Required | [^A] | | terrainsCanBeBuiltOn | List of Strings | empty | Terrains that this improvement can be built on [^B]. Removable terrain features will need to be removed before building an improvement [^C]. Must be in [Terrains.json](#terrainsjson) | | techRequired | String | none | The name of the technology required to build this improvement | +| replaces | String | none | The name of a improvement that should be replaced by this improvement. Must be in [TileImprovements.json](#TileImprovementsjson) | | uniqueTo | String | none | The name of the nation this improvement is unique for | | [``](#stats) | Integer | 0 | Per-turn bonus yield for the tile | | turnsToBuild | Integer | -1 | Number of turns a worker spends building this. If -1, the improvement is unbuildable [^D]. If 0, the improvement is always built in one turn |