Merge branch 'yairm210:master' into updated-so-you-can-build-naval-unit-on-water-tile

This commit is contained in:
General_E 2025-08-29 14:14:57 +02:00 committed by GitHub
commit 22935e661d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 121 additions and 80 deletions

View File

@ -3,7 +3,7 @@ package com.unciv.build
object BuildConfig {
const val appName = "Unciv"
const val appCodeNumber = 1158
const val appVersion = "4.17.17"
const val appCodeNumber = 1159
const val appVersion = "4.17.17-patch1"
const val identifier = "com.unciv.app"
}

View File

@ -494,7 +494,7 @@ open class UncivGame(val isConsoleMode: Boolean = false) : Game(), PlatformSpeci
companion object {
//region AUTOMATICALLY GENERATED VERSION DATA - DO NOT CHANGE THIS REGION, INCLUDING THIS COMMENT
val VERSION = Version("4.17.17", 1158)
val VERSION = Version("4.17.17-patch1", 1159)
//endregion
/** Global reference to the one Gdx.Game instance created by the platform launchers - do not use without checking [isCurrentInitialized] first. */

View File

@ -297,6 +297,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) {
} else value += when {
building.hasUnique(UniqueType.CreatesOneImprovement) -> 5f // District-type buildings, should be weighed by the stats (incl. adjacencies) of the improvement
building.hasUnique(UniqueType.ProvidesResources) -> 2f // Should be weighed by how much we need the resources
building.hasUnique(UniqueType.StatPercentFromObjectToResource) -> 1.5f // Should be weighed by the amount of active improvementFilter/buildingFilter in the city
else -> 0f
}
return value

View File

@ -130,6 +130,7 @@ object ReligiousUnitAutomation {
return null
val holyCity = unit.civ.religionManager.getHolyCity()
// Our own holy city was taken over!
if (holyCity != null && holyCity.religion.getMajorityReligion() != unit.civ.religionManager.religion!!)
return holyCity
@ -137,12 +138,22 @@ object ReligiousUnitAutomation {
if (blockedHolyCity != null)
return blockedHolyCity
return unit.civ.cities.asSequence()
.filter { it.religion.getMajorityReligion() != null }
.filter { it.religion.getMajorityReligion()!! != unit.civ.religionManager.religion }
// Don't go if it takes too long
// Find cities
val relevantCities = unit.civ.gameInfo.getCities()
.filter { it.getCenterTile().isExplored(unit.civ) } // Cities we know about
// Someone else is controlling this city
.filter {
val majorityReligion = it.religion.getMajorityReligion()
majorityReligion != null && majorityReligion != unit.civ.religionManager.religion
}
val closeCity = relevantCities
.filter { it.getCenterTile().aerialDistanceTo(unit.currentTile) <= 20 }
.maxByOrNull { it.religion.getPressureDeficit(unit.civ.religionManager.religion?.name) }
// Find the city that we're the closest to converting
.minByOrNull { it.religion.getPressureDeficit(unit.civ.religionManager.religion?.name) }
if (closeCity != null) return closeCity
return relevantCities.minByOrNull { it.religion.getPressureDeficit(unit.civ.religionManager.religion?.name) }
}

View File

@ -170,9 +170,9 @@ class City : IsPartOfGameInfoSerialization, INamed {
@Readonly fun getCenterTileOrNull(): Tile? = if (::centerTile.isInitialized) centerTile else null
@Readonly fun getTiles(): Sequence<Tile> = tiles.asSequence().map { tileMap[it] }
@Readonly fun getWorkableTiles() = tilesInRange.asSequence().filter { it.getOwner() == civ }
@Readonly fun getWorkedTiles(): Sequence<Tile> = workedTiles.asSequence().map { tileMap[it] }
@Readonly fun isWorked(tile: Tile) = workedTiles.contains(tile.position)
@Readonly fun isCapital(): Boolean = cityConstructions.builtBuildingUniqueMap.hasUnique(UniqueType.IndicatesCapital, state)
@Readonly fun isCoastal(): Boolean = centerTile.isCoastalTile()

View File

@ -1,12 +1,14 @@
package com.unciv.logic.city
import com.unciv.logic.map.tile.Tile
import com.unciv.models.stats.Stat
import com.unciv.models.ruleset.tile.ResourceSupplyList
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.unique.GameContext
import com.unciv.models.ruleset.unique.UniqueType
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly
import com.unciv.models.ruleset.unique.UniqueParameterType
object CityResources {
@ -14,7 +16,7 @@ object CityResources {
@Readonly
fun getResourcesGeneratedByCity(city: City, resourceModifiers: Map<String, Float>): ResourceSupplyList {
@LocalState val cityResources = getResourcesGeneratedByCityNotIncludingBuildings(city, resourceModifiers)
cityResources += getCityResourcesGeneratedFromUniqueBuildings(city, resourceModifiers)
cityResources += getCityResourcesGeneratedFromUniques(city, resourceModifiers, false)
return cityResources
}
@ -30,7 +32,7 @@ object CityResources {
// We can't use getResourcesGeneratedByCity directly, because that would include the resources generated by buildings -
// which are part of the civ-wide uniques, so we'd be getting them twice!
// This way we get them once, but it is ugly, I welcome other ideas :/
cityResources.add(getCityResourcesFromCiv(city, resourceModifers))
cityResources.add(getCityResourcesGeneratedFromUniques(city, resourceModifers, true))
cityResources.removeAll { !it.resource.isCityWide }
@ -56,20 +58,6 @@ object CityResources {
return cityResources
}
@Readonly
private fun getCityResourcesGeneratedFromUniqueBuildings(city: City, resourceModifer: Map<String, Float>): ResourceSupplyList {
val buildingResources = ResourceSupplyList()
for (unique in city.getMatchingUniques(UniqueType.ProvidesResources, city.state, false)) { // E.G "Provides [1] [Iron]"
val resource = city.getRuleset().tileResources[unique.params[1]]
?: continue
buildingResources.add(
resource, unique.getSourceNameForUser(),
(unique.params[0].toFloat() * resourceModifer[resource.name]!!).toInt()
)
}
return buildingResources
}
/** Gets the number of resources available to this city
* Accommodates both city-wide and civ-wide resources */
@Readonly
@ -129,18 +117,46 @@ object CityResources {
}
@Readonly
private fun getCityResourcesFromCiv(city: City, resourceModifers: HashMap<String, Float>): ResourceSupplyList {
val resourceSupplyList = ResourceSupplyList()
// This includes the uniques from buildings, from this and all other cities
for (unique in city.getMatchingUniques(UniqueType.ProvidesResources, city.state)) { // E.G "Provides [1] [Iron]"
private fun getCityResourcesGeneratedFromUniques(city: City, resourceModifers: Map<String, Float>, includeCivUniques: Boolean = true): ResourceSupplyList {
val buildingResources = ResourceSupplyList()
for (unique in city.getMatchingUniques(UniqueType.ProvidesResources, city.state, includeCivUniques)) { // E.G "Provides [1] [Iron]"
val resource = city.getRuleset().tileResources[unique.params[1]]
?: continue
resourceSupplyList.add(
buildingResources.add(
resource, unique.getSourceNameForUser(),
(unique.params[0].toFloat() * resourceModifers[resource.name]!!).toInt()
)
}
return resourceSupplyList
// StatPercentFromObjectToResource - Example: "[50]% of [Culture] from every [improvementFilter/buildingFilter] in the city added to [Iron]"
for (unique in city.getMatchingUniques(UniqueType.StatPercentFromObjectToResource, city.state, includeCivUniques)) {
val resource = city.getRuleset().tileResources[unique.params[3]] ?: continue
val stat = Stat.safeValueOf(unique.params[1]) ?: continue
val filter = unique.params[2]
var amount = 0.0
// Building Filter
if (UniqueParameterType.BuildingFilter.isKnownValue(filter, city.getRuleset())) {
amount += city.cityConstructions.getBuiltBuildings()
.filter { it.isStatRelated(stat) && it.matchesFilter(filter, city.state) }
.sumOf { it.getStats(city)[stat].toDouble() }
}
// Improvement Filter
if (UniqueParameterType.ImprovementFilter.isKnownValue(filter, city.getRuleset())) {
amount += city.getWorkedTiles()
.mapNotNull { it.getUnpillagedTileImprovement() }
.filter { it[stat] > 0f && it.matchesFilter(filter, city.state) }
.sumOf { it[stat].toDouble() }
}
if (amount > 0.0) {
amount *= unique.params[0].toDouble() / 100.0 * (resourceModifers[resource.name] ?: 1f).toDouble()
buildingResources.add(resource, unique.getSourceNameForUser(), amount.toInt())
}
}
return buildingResources
}
@Readonly

View File

@ -330,7 +330,8 @@ class CityReligionManager : IsPartOfGameInfoSerialization {
return pressure.toInt()
}
/** Calculates how much pressure this religion is lacking compared to the majority religion */
/** Calculates how much pressure this religion is lacking compared to the majority religion
* That is, if we gain more than this, we'll be the majority */
@Readonly
fun getPressureDeficit(otherReligion: String?): Int {
val pressures = getPressures()

View File

@ -267,13 +267,8 @@ class UnitMovement(val unit: MapUnit) {
* @return The tile that we reached this turn
*/
fun headTowards(destination: Tile): Tile {
val escortUnit = if (unit.isEscorting()) unit.getOtherEscortUnit() else null
val startTile = unit.getTile()
val destinationTileThisTurn = getTileToMoveToThisTurn(destination)
moveToTile(destinationTileThisTurn)
if (startTile != unit.getTile() && escortUnit != null) {
escortUnit.movement.headTowards(unit.getTile())
}
return unit.currentTile
}
@ -748,10 +743,6 @@ class UnitMovement(val unit: MapUnit) {
movementCostCache: HashMap<Pair<Tile, Tile>, Float> = HashMap(),
includeOtherEscortUnit: Boolean = true
): PathsToTilesWithinTurn {
// val cacheResults = pathfindingCache.getDistanceToTiles(considerZoneOfControl)
// if (cacheResults != null) {
// return cacheResults
// }
val distanceToTiles = getMovementToTilesAtPosition(
unit.currentTile.position,
unit.currentMovement,
@ -762,8 +753,6 @@ class UnitMovement(val unit: MapUnit) {
includeOtherEscortUnit
)
pathfindingCache.setDistanceToTiles(considerZoneOfControl, distanceToTiles)
return distanceToTiles
}
@ -838,14 +827,13 @@ class UnitMovement(val unit: MapUnit) {
class PathfindingCache(private val unit: MapUnit) {
private var shortestPathCache = listOf<Tile>()
private var destination: Tile? = null
private val distanceToTilesCache = mutableMapOf<Boolean, PathsToTilesWithinTurn>()
private var movement = -1f
private var currentTile: Tile? = null
/** Check if the caches are valid (only checking if the unit has moved or consumed movement points;
* the isPlayerCivilization check is performed in the functions because we want isValid() == false
* to have a specific behavior) */
private fun isValid(): Boolean = (movement == unit.currentMovement) && (unit.getTile() == currentTile)
@Readonly private fun isValid(): Boolean = (movement == unit.currentMovement) && (unit.getTile() == currentTile)
fun getShortestPathCache(destination: Tile): List<Tile> {
if (unit.civ.isHuman()) return listOf()
@ -863,23 +851,7 @@ class PathfindingCache(private val unit: MapUnit) {
}
}
fun getDistanceToTiles(zoneOfControl: Boolean): PathsToTilesWithinTurn? {
if (unit.civ.isHuman()) return null
if (isValid())
return distanceToTilesCache[zoneOfControl]
return null
}
fun setDistanceToTiles(zoneOfControl: Boolean, paths: PathsToTilesWithinTurn) {
if (unit.civ.isHuman()) return
if (!isValid()) {
clear() // we want to reset the entire cache at this point
}
distanceToTilesCache[zoneOfControl] = paths
}
fun clear() {
distanceToTilesCache.clear()
movement = unit.currentMovement
currentTile = unit.getTile()
destination = null

View File

@ -143,7 +143,11 @@ class TileResource : RulesetStatsObject(), GameResource {
val buildingsThatProvideThis = ruleset.buildings.values
.filter { building ->
building.uniqueObjects.any { unique ->
unique.type == UniqueType.ProvidesResources && unique.params[1] == name
when (unique.type) {
UniqueType.ProvidesResources -> unique.params[1] == name
UniqueType.StatPercentFromObjectToResource -> unique.params[3] == name
else -> false
}
}
}
if (buildingsThatProvideThis.isNotEmpty()) {

View File

@ -506,7 +506,7 @@ enum class UniqueParameterType(
/**Used by [UniqueType.ConditionalCityReligion]*/
ReligionFilter("religionFilter", "major") {
override val staticKnownValues = setOf("any", "major", "enhanced", "your", "foreign","enemy")
override val staticKnownValues = setOf("any", "major", "enhanced", "your", "foreign", "enemy")
override fun isKnownValue(parameterText: String, ruleset: Ruleset): Boolean {
return when (parameterText) {
in staticKnownValues -> true

View File

@ -102,7 +102,7 @@ object UniqueTriggerActivation {
if (timingConditional != null) {
return {
civInfo.temporaryUniques.add(TemporaryUnique(unique, timingConditional.params[0].toInt()))
if (unique.type in setOf(UniqueType.ProvidesResources, UniqueType.ConsumesResources))
if (unique.type in setOf(UniqueType.ProvidesResources, UniqueType.ConsumesResources, UniqueType.StatPercentFromObjectToResource))
civInfo.cache.updateCivResources()
true
}

View File

@ -47,6 +47,7 @@ enum class UniqueType(
StatPercentBonus("[relativeAmount]% [stat]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
StatPercentBonusCities("[relativeAmount]% [stat] [cityFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
StatPercentFromObject("[relativeAmount]% [stat] from every [tileFilter/buildingFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
StatPercentFromObjectToResource("[positiveAmount]% of [stat] from every [improvementFilter/buildingFilter] in the city added to [resource]", UniqueTarget.Building),
AllStatsPercentFromObject("[relativeAmount]% Yield from every [tileFilter/buildingFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
StatPercentFromReligionFollowers("[relativeAmount]% [stat] from every follower, up to [relativeAmount]%", UniqueTarget.FollowerBelief, UniqueTarget.FounderBelief),
BonusStatsFromCityStates("[relativeAmount]% [stat] from City-States", UniqueTarget.Global),

View File

@ -183,7 +183,7 @@ class UniqueValidator(val ruleset: Ruleset) {
}
private val resourceUniques = setOf(UniqueType.ProvidesResources, UniqueType.ConsumesResources,
UniqueType.PercentResourceProduction)
UniqueType.PercentResourceProduction, UniqueType.StatPercentFromObjectToResource)
private val resourceConditionals = setOf(
UniqueType.ConditionalWithResource,
UniqueType.ConditionalWithoutResource,

View File

@ -46,7 +46,7 @@ Allows filtering for specific nations. Used by [ModOptions.nationsToRemove](Mod-
Allowed values:
- `All`
- `All`, `all`
- `City-States`, `City-State`
- `Major`
- Nation name
@ -68,11 +68,11 @@ Allowed values:
- `non-air` for non-air non-missile units
- `Military`, `military units`
- `Civilian`, `civilian units`
- `All`
- `All`, `all`
- `Melee`
- `Ranged`
- `Nuclear Weapon`
- `Great Person`, `Great`
- `Great Person`
- `Embarked`
- Matching [technologyfilter](#technologyfilter) for the tech this unit requires - e.g. `Modern Era`
- Any exact unique the unit has
@ -88,11 +88,12 @@ Allowed values:
- Any matching [baseUnitFilter](#baseunitfilter)
- Any [civFilter](#civfilter) matching the owner
- Any unique the unit has - also includes uniques not caught by the [baseUnitFilter](#baseunitfilter), for example promotions
- Any promotion name
- Any promotion name, or an exact unique a promotion has
- `Wounded`
- `Embarked`
- `City-State`
- `Barbarians`, `Barbarian`
- `Non-City`
- Again, any combination of the above is also allowed, e.g. `[{Wounded} {Water}]` units.
You can check this in-game using the console with the `unit checkfilter <filter>` command
@ -103,7 +104,7 @@ Allows to only activate a unique for certain buildings.
Allowed values:
- `All`
- `All`, `all`
- `Buildings`, `Building`
- `Wonder`, `Wonders`
- `National Wonder`, `National`
@ -124,7 +125,7 @@ Allowed values:
cityFilters allow us to choose the range of cities affected by this unique:
- `in this city`
- `in all cities`
- `in all cities`, `All`, `all` - Generally applies to all cities owned by the relevant civ
- `in your cities`, `Your`
- `in all coastal cities`, `Coastal`
- `in capital`, `Capital`
@ -137,6 +138,7 @@ cityFilters allow us to choose the range of cities affected by this unique:
- `in foreign cities`, `Foreign`
- `in annexed cities`, `Annexed`
- `in puppeted cities`, `Puppeted`
- `in resisting cities`, `Resisting`
- `in cities being razed`, `Razing`
- `in holy cities`, `Holy`
- `in City-State cities`
@ -155,8 +157,10 @@ For filtering a specific improvement.
Allowed values:
- improvement name
- `All`
- `Great Improvements`, `Great`
- An exact unique the improvement has (e.g.: `spaceship improvement`)
- `Improvement`
- `All`, `all`
- `Great Improvement`, `Great`
- `All Road` - for Roads & Railroads
## populationFilter
@ -189,7 +193,7 @@ For filtering specific relgions
Allowed values:
- `All` or `all`
- `All`, `all`
- `[policyBranchName] branch`
- The name of the policy
- A unique the Policy has (verbatim, no placeholders)
@ -238,7 +242,8 @@ These can be strung together with ", " between them, for example: `+2 Production
Allowed values:
- Resource name
- `any`, `all`
- `any`
- `All`, `all`
- Resource type: `Strategic`, `Luxury`, `Bonus`
- Stat provided by the resource when improved (e.g. `Food`)
@ -270,7 +275,7 @@ At the moment only implemented for [ModOptions.techsToRemove](Mod-file-structure
Allowed values:
- `All`
- `All`, `all`
- The name of an Era
- The name of a Technology
- A unique a Technology has (verbatim, no placeholders)
@ -290,19 +295,23 @@ Allowed values:
- Natural wonder
- A [nationFilter](#nationfilter) matching the tile owner
- Or the filter is a constant string choosing a derived test:
- `All`
- `All`, `all`
- `Terrain`
- `Water`, `Land`
- `Coastal` (at least one direct neighbor is a coast)
- `River` (as in all 'river on tile' contexts, it means 'adjacent to a river on at least one side')
- `Open terrain`, `Rough terrain` (note all terrain not having the rough unique is counted as open)
- `Friendly Land` - land belonging to you, or other civs with open borders to you
- `Friendly Land`, `Friendly` - land belonging to you, or other civs with open borders to you
- `Foreign Land` - any land that isn't friendly land
- `Enemy Land` - any land belonging to a civ you are at war with
- `Enemy Land`, `Enemy` - any land belonging to a civ you are at war with
- `your` - land belonging to you
- `unowned` - land that is not owned by any civ
- `Unowned` - land that is not owned by any civ
- `Water resource`, `Strategic resource`, `Luxury resource`, `Bonus resource`, `resource`
- `Natural Wonder` (as opposed to above which means testing for a specific Natural Wonder by name, this tests for any of them)
- `Featureless`
- `Fresh Water`
- `non-fresh water`
- `Impassible`
Please note all of these are _case-sensitive_.
@ -316,6 +325,7 @@ Allowed values:
- [terrainFilter](#terrainfilter) for this tile
- [improvementFilter](#improvementfilter) for this tile
- [civFilter](#civfilter) of the civilization who owns this tile
- `Improvement` or `improved` for tiles with any improvements
- `unimproved` for tiles with no improvement
- `pillaged` for pillaged tiles

View File

@ -1523,6 +1523,11 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
Applicable to: Nation, Tech, Policy, FounderBelief, FollowerBelief, Building, Unit, UnitType, Promotion, Terrain, Improvement, Resource, Ruins, Speed, Difficulty, EventChoice
## Building uniques
??? example "[positiveAmount]% of [stat] from every [improvementFilter/buildingFilter] in the city added to [resource]"
Example: "[3]% of [Culture] from every [All Road] in the city added to [Iron]"
Applicable to: Building
??? example "Consumes [amount] [resource]"
Example: "Consumes [3] [Iron]"

View File

@ -166,6 +166,26 @@ class ResourceTests {
assertEquals("4 Iron from Buildings", resources[0].toString())
}
@Test
fun `should handle StatPercentFromObjectToResource with a buildingFilter`() {
city.cityConstructions.addBuilding("Monument")
var building = game.createBuilding("[300]% of [Culture] from every [Monument] in the city added to [Iron]")
city.cityConstructions.addBuilding(building)
assertEquals(6, city.getAvailableResourceAmount("Iron")) // 2 Culture * 3
}
@Test
fun `should handle StatPercentFromObjectToResource with a improvementFilter`() {
val tile = game.tileMap[1,1]
tile.resource = "Wheat"
tile.resourceAmount = 1
tile.setImprovement("Farm")
city.population.addPopulation(5) // Add population, since the tile needs to be worked
var building = game.createBuilding("[300]% of [Food] from every [Farm] in the city added to [Iron]")
city.cityConstructions.addBuilding(building)
assertEquals(3, city.getAvailableResourceAmount("Iron"))
}
@Test
fun `should reduce resources due to buildings`() {
// given