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
This commit is contained in:
SomeTroglodyte 2023-03-13 16:02:08 +01:00 committed by GitHub
parent 9eee47a628
commit db08c30363
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 76 additions and 56 deletions

View File

@ -281,7 +281,8 @@
{ {
"name": "City center", "name": "City center",
"terrainsCanBeBuiltOn": ["Land"], "terrainsCanBeBuiltOn": ["Land"],
"uniques": ["Unpillagable", "Irremovable", "Unbuildable"], "uniques": ["Ensures a minimum tile yield of [+2 Food, +1 Production]",
"Unpillagable", "Irremovable", "Unbuildable"],
"civilopediaText": [ "civilopediaText": [
{"text":"Marks the center of a city"}, {"text":"Marks the center of a city"},
{"text":"Appearance changes with the technological era of the owning civilization"} {"text":"Appearance changes with the technological era of the owning civilization"}

View File

@ -270,7 +270,8 @@
{ {
"name": "City center", "name": "City center",
"terrainsCanBeBuiltOn": ["Land"], "terrainsCanBeBuiltOn": ["Land"],
"uniques": ["Unpillagable", "Irremovable", "Unbuildable"], "uniques": ["Ensures a minimum tile yield of [+2 Food, +1 Production]",
"Unpillagable", "Irremovable", "Unbuildable"],
"civilopediaText": [ "civilopediaText": [
{"text":"Marks the center of a city"}, {"text":"Marks the center of a city"},
{"text":"Appearance changes with the technological era of the owning civilization"} {"text":"Appearance changes with the technological era of the owning civilization"}

View File

@ -5,7 +5,7 @@
"tileScales": { "tileScales": {
"Atoll":0.35, "Atoll":0.35,
"City center":0.7, "City center":0.7,
"Faulout":0.35, "Fallout":0.35,
"Flood plains":0.35, "Flood plains":0.35,
"Forest":0.35, "Forest":0.35,
"Hill":0.35, "Hill":0.35,

View File

@ -38,6 +38,7 @@ object Constants {
const val freshWaterFilter = "Fresh Water" const val freshWaterFilter = "Fresh Water"
const val barbarianEncampment = "Barbarian encampment" const val barbarianEncampment = "Barbarian encampment"
const val cityCenter = "City center"
const val peaceTreaty = "Peace Treaty" const val peaceTreaty = "Peace Treaty"
const val researchAgreement = "Research Agreement" const val researchAgreement = "Research Agreement"

View File

@ -17,7 +17,9 @@ import com.unciv.models.metadata.Player
import com.unciv.models.ruleset.ModOptionsConstants import com.unciv.models.ruleset.ModOptionsConstants
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stats
import com.unciv.models.translations.equalsPlaceholderText import com.unciv.models.translations.equalsPlaceholderText
import com.unciv.models.translations.getPlaceholderParameters import com.unciv.models.translations.getPlaceholderParameters
import com.unciv.utils.debug import com.unciv.utils.debug
@ -329,9 +331,15 @@ object GameStarter {
var startingUnits: MutableList<String> var startingUnits: MutableList<String>
var eraUnitReplacement: String 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<Tile, Float>(tileMap.values.size) val startScores = HashMap<Tile, Float>(tileMap.values.size)
for (tile in tileMap.values) { for (tile in tileMap.values) {
startScores[tile] = tile.stats.getTileStartScore() startScores[tile] = tile.stats.getTileStartScore(cityCenterMinStats)
} }
val allCivs = gameInfo.civilizations.filter { !it.isBarbarian() } val allCivs = gameInfo.civilizations.filter { !it.isBarbarian() }
val landTilesInBigEnoughGroup = getCandidateLand(allCivs.size, tileMap, startScores) val landTilesInBigEnoughGroup = getCandidateLand(allCivs.size, tileMap, startScores)

View File

@ -18,28 +18,11 @@ class TileStatFunctions(val tile: Tile) {
fun getTileStats(city: City?, observingCiv: Civilization?, fun getTileStats(city: City?, observingCiv: Civilization?,
localUniqueCache: LocalUniqueCache = LocalUniqueCache(false) localUniqueCache: LocalUniqueCache = LocalUniqueCache(false)
): Stats { ): 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) 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) { if (city != null) {
var tileUniques = city.getMatchingUniques(UniqueType.StatsFromTiles, StateForConditionals.IgnoreConditionals) var tileUniques = city.getMatchingUniques(UniqueType.StatsFromTiles, StateForConditionals.IgnoreConditionals)
.filter { city.matchesFilter(it.params[2]) } .filter { city.matchesFilter(it.params[2]) }
@ -76,14 +59,16 @@ class TileStatFunctions(val tile: Tile) {
if (stats.gold != 0f && observingCiv.goldenAges.isGoldenAge()) if (stats.gold != 0f && observingCiv.goldenAges.isGoldenAge())
stats.gold++ stats.gold++
}
if (tile.isCityCenter()) { if (improvement != null) {
if (stats.food < 2) stats.food = 2f val ensureMinUnique = improvement
if (stats.production < 1) stats.production = 1f .getMatchingUniques(UniqueType.EnsureMinimumStats, stateForConditionals)
.firstOrNull()
if (ensureMinUnique != null) minimumStats = ensureMinUnique.stats
}
} }
for ((stat, value) in stats) stats.coerceAtLeast(minimumStats) // Minimum 0 or as defined by City center
if (value < 0f) stats[stat] = 0f
for ((stat, value) in getTilePercentageStats(observingCiv, city)) { for ((stat, value) in getTilePercentageStats(observingCiv, city)) {
stats[stat] *= value.toPercent() stats[stat] *= value.toPercent()
@ -92,6 +77,30 @@ class TileStatFunctions(val tile: Tile) {
return stats 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 // Only gets the tile percentage bonus, not the improvement percentage bonus
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
fun getTilePercentageStats(observingCiv: Civilization?, city: City?): Stats { fun getTilePercentageStats(observingCiv: Civilization?, city: City?): Stats {
@ -132,10 +141,12 @@ class TileStatFunctions(val tile: Tile) {
return stats return stats
} }
fun getTileStartScore(): Float { fun getTileStartScore(cityCenterMinStats: Stats): Float {
var sum = 0f var sum = 0f
for (closeTile in tile.getTilesInDistance(2)) { 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 sum += tileYield
if (closeTile in tile.neighbors) if (closeTile in tile.neighbors)
sum += tileYield sum += tileYield
@ -155,25 +166,12 @@ class TileStatFunctions(val tile: Tile) {
return sum return sum
} }
private fun getTileStartYield(isCenter: Boolean): Float { private fun getTileStartYield(minimumStats: Stats) =
var stats = tile.getBaseTerrain().cloneStats() getTerrainStats().run {
if (tile.resource != null) add(tile.tileResource)
for (terrainFeatureBase in tile.terrainFeatureObjects) { coerceAtLeast(minimumStats)
if (terrainFeatureBase.overrideStats) food + production + gold
stats = terrainFeatureBase.cloneStats()
else
stats.add(terrainFeatureBase)
} }
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) // Also multiplies the stats by the percentage bonus for improvements (but not for tiles)

View File

@ -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), StatsFromTradeRoute("[stats] from each Trade Route", UniqueTarget.Global, UniqueTarget.FollowerBelief),
StatsFromGlobalCitiesFollowingReligion("[stats] for each global city following this religion", UniqueTarget.FounderBelief), StatsFromGlobalCitiesFollowingReligion("[stats] for each global city following this religion", UniqueTarget.FounderBelief),
StatsFromGlobalFollowers("[stats] from every [amount] global followers [cityFilter]", 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 // Stat percentage boosts
StatPercentBonus("[relativeAmount]% [stat]", UniqueTarget.Global, UniqueTarget.FollowerBelief), StatPercentBonus("[relativeAmount]% [stat]", UniqueTarget.Global, UniqueTarget.FollowerBelief),

View File

@ -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 { fun toStringForNotifications() = this.joinToString {
(if (it.value > 0) "+" else "") + it.value.toInt() + " " + it.key.toString().tr(Constants.english) (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 { fun parse(string: String): Stats {
val toReturn = Stats() val toReturn = Stats()
val statsWithBonuses = string.split(", ") val statsWithBonuses = string.split(", ")
for(statWithBonuses in statsWithBonuses){ for(statWithBonuses in statsWithBonuses) {
val match = statRegex.matchEntire(statWithBonuses)!! val match = statRegex.matchEntire(statWithBonuses)!!
val statName = match.groupValues[3] val statName = match.groupValues[3]
val statAmount = match.groupValues[2].toFloat() * (if (match.groupValues[1] == "-") -1 else 1) val statAmount = match.groupValues[2].toFloat() * (if (match.groupValues[1] == "-") -1 else 1)
@ -241,6 +241,9 @@ open class Stats(
} }
return toReturn return toReturn
} }
val ZERO = Stats()
val DefaultCityCenterMinimum = Stats(food = 2f, production = 1f)
} }
} }

View File

@ -248,7 +248,7 @@ class MapEditorEditImprovementsTab(
companion object { companion object {
//todo This should really be easier, the attributes should allow such a test in one go //todo This should really be easier, the attributes should allow such a test in one go
private val disallowImprovements = listOf( 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 { private fun TileImprovement.group() = when {
RoadStatus.values().any { it.name == name } -> 2 RoadStatus.values().any { it.name == name } -> 2

View File

@ -197,8 +197,8 @@ object UnitActions {
if (unit.civ.playerType != PlayerType.AI) if (unit.civ.playerType != PlayerType.AI)
UncivGame.Current.settings.addCompletedTutorialTask("Found city") UncivGame.Current.settings.addCompletedTutorialTask("Found city")
unit.civ.addCity(tile.position) unit.civ.addCity(tile.position)
if (tile.ruleset.tileImprovements.containsKey("City center")) if (tile.ruleset.tileImprovements.containsKey(Constants.cityCenter))
tile.changeImprovement("City center") tile.changeImprovement(Constants.cityCenter)
tile.removeRoad() tile.removeRoad()
if (hasActionModifiers) activateSideEffects(unit, unique) if (hasActionModifiers) activateSideEffects(unit, unique)

View File

@ -1491,6 +1491,11 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
Applicable to: Terrain Applicable to: Terrain
## Improvement uniques ## 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" ??? example "Can also be built on tiles adjacent to fresh water"
Applicable to: Improvement Applicable to: Improvement

View File

@ -1,6 +1,7 @@
package com.unciv.testing package com.unciv.testing
import com.badlogic.gdx.Gdx import com.badlogic.gdx.Gdx
import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.json.json import com.unciv.json.json
import com.unciv.logic.GameInfo import com.unciv.logic.GameInfo
@ -74,8 +75,8 @@ class SerializationTests {
val unit = civ.units.getCivUnits().first { it.hasUnique(UniqueType.FoundCity) } val unit = civ.units.getCivUnits().first { it.hasUnique(UniqueType.FoundCity) }
val tile = unit.getTile() val tile = unit.getTile()
unit.civ.addCity(tile.position) unit.civ.addCity(tile.position)
if (tile.ruleset.tileImprovements.containsKey("City center")) if (tile.ruleset.tileImprovements.containsKey(Constants.cityCenter))
tile.changeImprovement("City center") tile.changeImprovement(Constants.cityCenter)
unit.destroy() unit.destroy()
// Ensure some diplomacy objects are instantiated // Ensure some diplomacy objects are instantiated