From db08c30363ed2514c0eb5bbb747b32221b86bfaa Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Mon, 13 Mar 2023 16:02:08 +0100 Subject: [PATCH] Make City center minimum tile yields moddable (#8804) * Slight cleanup of TileStatFunctions * Make City center minimum tile yields moddable * Make City center minimum tile yields moddable - patch1 * Make City center minimum tile yields moddable - patch1 --- .../TileImprovements.json | 3 +- .../Civ V - Vanilla/TileImprovements.json | 3 +- android/assets/jsons/TileSets/Minimal.json | 2 +- core/src/com/unciv/Constants.kt | 1 + core/src/com/unciv/logic/GameStarter.kt | 10 ++- .../unciv/logic/map/tile/TileStatFunctions.kt | 88 +++++++++---------- .../unciv/models/ruleset/unique/UniqueType.kt | 2 + core/src/com/unciv/models/stats/Stats.kt | 7 +- .../tabs/MapEditorEditSubTabs.kt | 2 +- .../worldscreen/unit/actions/UnitActions.kt | 4 +- docs/Modders/uniques.md | 5 ++ .../com/unciv/testing/SerializationTests.kt | 5 +- 12 files changed, 76 insertions(+), 56 deletions(-) diff --git a/android/assets/jsons/Civ V - Gods & Kings/TileImprovements.json b/android/assets/jsons/Civ V - Gods & Kings/TileImprovements.json index 7aeb217c83..1095d344a8 100644 --- a/android/assets/jsons/Civ V - Gods & Kings/TileImprovements.json +++ b/android/assets/jsons/Civ V - Gods & Kings/TileImprovements.json @@ -281,7 +281,8 @@ { "name": "City center", "terrainsCanBeBuiltOn": ["Land"], - "uniques": ["Unpillagable", "Irremovable", "Unbuildable"], + "uniques": ["Ensures a minimum tile yield of [+2 Food, +1 Production]", + "Unpillagable", "Irremovable", "Unbuildable"], "civilopediaText": [ {"text":"Marks the center of a city"}, {"text":"Appearance changes with the technological era of the owning civilization"} diff --git a/android/assets/jsons/Civ V - Vanilla/TileImprovements.json b/android/assets/jsons/Civ V - Vanilla/TileImprovements.json index 2a5d6b18a5..ab5bcbf452 100644 --- a/android/assets/jsons/Civ V - Vanilla/TileImprovements.json +++ b/android/assets/jsons/Civ V - Vanilla/TileImprovements.json @@ -270,7 +270,8 @@ { "name": "City center", "terrainsCanBeBuiltOn": ["Land"], - "uniques": ["Unpillagable", "Irremovable", "Unbuildable"], + "uniques": ["Ensures a minimum tile yield of [+2 Food, +1 Production]", + "Unpillagable", "Irremovable", "Unbuildable"], "civilopediaText": [ {"text":"Marks the center of a city"}, {"text":"Appearance changes with the technological era of the owning civilization"} diff --git a/android/assets/jsons/TileSets/Minimal.json b/android/assets/jsons/TileSets/Minimal.json index 2c7b6f92b9..ba41800f4d 100644 --- a/android/assets/jsons/TileSets/Minimal.json +++ b/android/assets/jsons/TileSets/Minimal.json @@ -5,7 +5,7 @@ "tileScales": { "Atoll":0.35, "City center":0.7, - "Faulout":0.35, + "Fallout":0.35, "Flood plains":0.35, "Forest":0.35, "Hill":0.35, diff --git a/core/src/com/unciv/Constants.kt b/core/src/com/unciv/Constants.kt index e674969c61..63b5d43311 100644 --- a/core/src/com/unciv/Constants.kt +++ b/core/src/com/unciv/Constants.kt @@ -38,6 +38,7 @@ object Constants { const val freshWaterFilter = "Fresh Water" const val barbarianEncampment = "Barbarian encampment" + const val cityCenter = "City center" const val peaceTreaty = "Peace Treaty" const val researchAgreement = "Research Agreement" diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index 94dcd48c51..c3529548b6 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -17,7 +17,9 @@ import com.unciv.models.metadata.Player import com.unciv.models.ruleset.ModOptionsConstants import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetCache +import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.models.stats.Stats import com.unciv.models.translations.equalsPlaceholderText import com.unciv.models.translations.getPlaceholderParameters import com.unciv.utils.debug @@ -329,9 +331,15 @@ object GameStarter { var startingUnits: MutableList var eraUnitReplacement: String + val cityCenterMinStats = sequenceOf(ruleSet.tileImprovements[Constants.cityCenter]) + .filterNotNull() + .flatMap { it.getMatchingUniques(UniqueType.EnsureMinimumStats, StateForConditionals.IgnoreConditionals) } + .firstOrNull() + ?.stats ?: Stats.DefaultCityCenterMinimum + val startScores = HashMap(tileMap.values.size) for (tile in tileMap.values) { - startScores[tile] = tile.stats.getTileStartScore() + startScores[tile] = tile.stats.getTileStartScore(cityCenterMinStats) } val allCivs = gameInfo.civilizations.filter { !it.isBarbarian() } val landTilesInBigEnoughGroup = getCandidateLand(allCivs.size, tileMap, startScores) diff --git a/core/src/com/unciv/logic/map/tile/TileStatFunctions.kt b/core/src/com/unciv/logic/map/tile/TileStatFunctions.kt index f9788cf652..f88fd897c0 100644 --- a/core/src/com/unciv/logic/map/tile/TileStatFunctions.kt +++ b/core/src/com/unciv/logic/map/tile/TileStatFunctions.kt @@ -18,28 +18,11 @@ class TileStatFunctions(val tile: Tile) { fun getTileStats(city: City?, observingCiv: Civilization?, localUniqueCache: LocalUniqueCache = LocalUniqueCache(false) ): Stats { - var stats = tile.getBaseTerrain().cloneStats() + val stats = getTerrainStats() + var minimumStats = if (tile.isCityCenter()) Stats.DefaultCityCenterMinimum else Stats.ZERO val stateForConditionals = StateForConditionals(civInfo = observingCiv, city = city, tile = tile) - for (terrainFeatureBase in tile.terrainFeatureObjects) { - when { - terrainFeatureBase.hasUnique(UniqueType.NullifyYields) -> - return terrainFeatureBase.cloneStats() - terrainFeatureBase.overrideStats -> stats = terrainFeatureBase.cloneStats() - else -> stats.add(terrainFeatureBase) - } - } - - if (tile.naturalWonder != null) { - val wonderStats = tile.getNaturalWonder().cloneStats() - - if (tile.getNaturalWonder().overrideStats) - stats = wonderStats - else - stats.add(wonderStats) - } - if (city != null) { var tileUniques = city.getMatchingUniques(UniqueType.StatsFromTiles, StateForConditionals.IgnoreConditionals) .filter { city.matchesFilter(it.params[2]) } @@ -76,14 +59,16 @@ class TileStatFunctions(val tile: Tile) { if (stats.gold != 0f && observingCiv.goldenAges.isGoldenAge()) stats.gold++ - } - if (tile.isCityCenter()) { - if (stats.food < 2) stats.food = 2f - if (stats.production < 1) stats.production = 1f + + if (improvement != null) { + val ensureMinUnique = improvement + .getMatchingUniques(UniqueType.EnsureMinimumStats, stateForConditionals) + .firstOrNull() + if (ensureMinUnique != null) minimumStats = ensureMinUnique.stats + } } - for ((stat, value) in stats) - if (value < 0f) stats[stat] = 0f + stats.coerceAtLeast(minimumStats) // Minimum 0 or as defined by City center for ((stat, value) in getTilePercentageStats(observingCiv, city)) { stats[stat] *= value.toPercent() @@ -92,6 +77,30 @@ class TileStatFunctions(val tile: Tile) { return stats } + /** Ensures each stat is >= [other].stat - modifies in place */ + private fun Stats.coerceAtLeast(other: Stats) { + for ((stat, value) in other) + if (this[stat] < value) this[stat] = value + } + + /** Gets basic stats to start off [getTileStats] or [getTileStartYield], independently mutable result */ + private fun getTerrainStats(): Stats { + var stats: Stats? = null + + // allTerrains iterates over base, natural wonder, then features + for (terrain in tile.allTerrains) { + when { + terrain.hasUnique(UniqueType.NullifyYields) -> + return terrain.cloneStats() + terrain.overrideStats || stats == null -> + stats = terrain.cloneStats() + else -> + stats.add(terrain) + } + } + return stats!! + } + // Only gets the tile percentage bonus, not the improvement percentage bonus @Suppress("MemberVisibilityCanBePrivate") fun getTilePercentageStats(observingCiv: Civilization?, city: City?): Stats { @@ -132,10 +141,12 @@ class TileStatFunctions(val tile: Tile) { return stats } - fun getTileStartScore(): Float { + fun getTileStartScore(cityCenterMinStats: Stats): Float { var sum = 0f for (closeTile in tile.getTilesInDistance(2)) { - val tileYield = closeTile.stats.getTileStartYield(closeTile == tile) + val tileYield = closeTile.stats.getTileStartYield( + if (closeTile == tile) cityCenterMinStats else Stats.ZERO + ) sum += tileYield if (closeTile in tile.neighbors) sum += tileYield @@ -155,25 +166,12 @@ class TileStatFunctions(val tile: Tile) { return sum } - private fun getTileStartYield(isCenter: Boolean): Float { - var stats = tile.getBaseTerrain().cloneStats() - - for (terrainFeatureBase in tile.terrainFeatureObjects) { - if (terrainFeatureBase.overrideStats) - stats = terrainFeatureBase.cloneStats() - else - stats.add(terrainFeatureBase) + private fun getTileStartYield(minimumStats: Stats) = + getTerrainStats().run { + if (tile.resource != null) add(tile.tileResource) + coerceAtLeast(minimumStats) + food + production + gold } - if (tile.resource != null) stats.add(tile.tileResource) - - if (stats.production < 0) stats.production = 0f - if (isCenter) { - if (stats.food < 2) stats.food = 2f - if (stats.production < 1) stats.production = 1f - } - - return stats.food + stats.production + stats.gold - } // Also multiplies the stats by the percentage bonus for improvements (but not for tiles) diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index 2477a0a84f..e6741238ad 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -100,6 +100,8 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags: StatsFromTradeRoute("[stats] from each Trade Route", UniqueTarget.Global, UniqueTarget.FollowerBelief), StatsFromGlobalCitiesFollowingReligion("[stats] for each global city following this religion", UniqueTarget.FounderBelief), StatsFromGlobalFollowers("[stats] from every [amount] global followers [cityFilter]", UniqueTarget.FounderBelief), + // Used for City center + EnsureMinimumStats("Ensures a minimum tile yield of [stats]", UniqueTarget.Improvement), // Stat percentage boosts StatPercentBonus("[relativeAmount]% [stat]", UniqueTarget.Global, UniqueTarget.FollowerBelief), diff --git a/core/src/com/unciv/models/stats/Stats.kt b/core/src/com/unciv/models/stats/Stats.kt index ca9906b402..ddfe462afc 100644 --- a/core/src/com/unciv/models/stats/Stats.kt +++ b/core/src/com/unciv/models/stats/Stats.kt @@ -150,7 +150,7 @@ open class Stats( } } - /** Since notificaitons are translated on the fly, when saving stats there we need to do so in English */ + /** Since notifications are translated on the fly, when saving stats there we need to do so in English */ fun toStringForNotifications() = this.joinToString { (if (it.value > 0) "+" else "") + it.value.toInt() + " " + it.key.toString().tr(Constants.english) } @@ -233,7 +233,7 @@ open class Stats( fun parse(string: String): Stats { val toReturn = Stats() val statsWithBonuses = string.split(", ") - for(statWithBonuses in statsWithBonuses){ + for(statWithBonuses in statsWithBonuses) { val match = statRegex.matchEntire(statWithBonuses)!! val statName = match.groupValues[3] val statAmount = match.groupValues[2].toFloat() * (if (match.groupValues[1] == "-") -1 else 1) @@ -241,6 +241,9 @@ open class Stats( } return toReturn } + + val ZERO = Stats() + val DefaultCityCenterMinimum = Stats(food = 2f, production = 1f) } } diff --git a/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorEditSubTabs.kt b/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorEditSubTabs.kt index a159f81c7c..a616d4cb9b 100644 --- a/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorEditSubTabs.kt +++ b/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorEditSubTabs.kt @@ -248,7 +248,7 @@ class MapEditorEditImprovementsTab( companion object { //todo This should really be easier, the attributes should allow such a test in one go private val disallowImprovements = listOf( - "City center", Constants.repair, Constants.remove, Constants.cancelImprovementOrder + Constants.cityCenter, Constants.repair, Constants.remove, Constants.cancelImprovementOrder ) private fun TileImprovement.group() = when { RoadStatus.values().any { it.name == name } -> 2 diff --git a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActions.kt b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActions.kt index 8d6c24c606..554c77567c 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActions.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActions.kt @@ -197,8 +197,8 @@ object UnitActions { if (unit.civ.playerType != PlayerType.AI) UncivGame.Current.settings.addCompletedTutorialTask("Found city") unit.civ.addCity(tile.position) - if (tile.ruleset.tileImprovements.containsKey("City center")) - tile.changeImprovement("City center") + if (tile.ruleset.tileImprovements.containsKey(Constants.cityCenter)) + tile.changeImprovement(Constants.cityCenter) tile.removeRoad() if (hasActionModifiers) activateSideEffects(unit, unique) diff --git a/docs/Modders/uniques.md b/docs/Modders/uniques.md index 1e9b388d9d..9178239d54 100644 --- a/docs/Modders/uniques.md +++ b/docs/Modders/uniques.md @@ -1491,6 +1491,11 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl Applicable to: Terrain ## Improvement uniques +??? example "Ensures a minimum tile yield of [stats]" + Example: "Ensures a minimum tile yield of [+1 Gold, +2 Production]" + + Applicable to: Improvement + ??? example "Can also be built on tiles adjacent to fresh water" Applicable to: Improvement diff --git a/tests/src/com/unciv/testing/SerializationTests.kt b/tests/src/com/unciv/testing/SerializationTests.kt index 695304caa6..eac88e097b 100644 --- a/tests/src/com/unciv/testing/SerializationTests.kt +++ b/tests/src/com/unciv/testing/SerializationTests.kt @@ -1,6 +1,7 @@ package com.unciv.testing import com.badlogic.gdx.Gdx +import com.unciv.Constants import com.unciv.UncivGame import com.unciv.json.json import com.unciv.logic.GameInfo @@ -74,8 +75,8 @@ class SerializationTests { val unit = civ.units.getCivUnits().first { it.hasUnique(UniqueType.FoundCity) } val tile = unit.getTile() unit.civ.addCity(tile.position) - if (tile.ruleset.tileImprovements.containsKey("City center")) - tile.changeImprovement("City center") + if (tile.ruleset.tileImprovements.containsKey(Constants.cityCenter)) + tile.changeImprovement(Constants.cityCenter) unit.destroy() // Ensure some diplomacy objects are instantiated