mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-15 10:18:26 +07:00
Allow improvements that don't need removal to build (#11299)
* Fix improvements that remove features only not checking for all terrains * Allow improvements that don't need removal to build without improving resource * spot the missing import * Add in tests * typo * Assert Forest is still there
This commit is contained in:
@ -443,7 +443,7 @@ class WorkerAutomation(
|
|||||||
return tile.tileResource.getImprovements().any { resourceImprovementName ->
|
return tile.tileResource.getImprovements().any { resourceImprovementName ->
|
||||||
if (resourceImprovementName !in potentialTileImprovements) return@any false
|
if (resourceImprovementName !in potentialTileImprovements) return@any false
|
||||||
val resourceImprovement = potentialTileImprovements[resourceImprovementName]!!
|
val resourceImprovement = potentialTileImprovements[resourceImprovementName]!!
|
||||||
tile.terrainFeatures.any { resourceImprovement.isAllowedOnFeature(it) }
|
tile.terrainFeatureObjects.any { resourceImprovement.isAllowedOnFeature(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -495,7 +495,7 @@ class Tile : IsPartOfGameInfoSerialization {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun matchesTerrainFilter(filter: String, observingCiv: Civilization? = null): Boolean {
|
fun matchesTerrainFilter(filter: String, observingCiv: Civilization? = null): Boolean {
|
||||||
return MultiFilter.multiFilter(filter, {matchesSingleTerrainFilter(it, observingCiv)})
|
return MultiFilter.multiFilter(filter, { matchesSingleTerrainFilter(it, observingCiv) })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Implements [UniqueParameterType.TerrainFilter][com.unciv.models.ruleset.unique.UniqueParameterType.TerrainFilter] */
|
/** Implements [UniqueParameterType.TerrainFilter][com.unciv.models.ruleset.unique.UniqueParameterType.TerrainFilter] */
|
||||||
@ -508,9 +508,6 @@ class Tile : IsPartOfGameInfoSerialization {
|
|||||||
"Land" -> isLand
|
"Land" -> isLand
|
||||||
Constants.coastal -> isCoastalTile()
|
Constants.coastal -> isCoastalTile()
|
||||||
Constants.river -> isAdjacentToRiver()
|
Constants.river -> isAdjacentToRiver()
|
||||||
naturalWonder -> true
|
|
||||||
"Open terrain" -> !isRoughTerrain()
|
|
||||||
"Rough terrain" -> isRoughTerrain()
|
|
||||||
|
|
||||||
"your" -> observingCiv != null && getOwner() == observingCiv
|
"your" -> observingCiv != null && getOwner() == observingCiv
|
||||||
"Foreign Land", "Foreign" -> observingCiv != null && !isFriendlyTerritory(observingCiv)
|
"Foreign Land", "Foreign" -> observingCiv != null && !isFriendlyTerritory(observingCiv)
|
||||||
@ -520,13 +517,11 @@ class Tile : IsPartOfGameInfoSerialization {
|
|||||||
resource -> observingCiv != null && hasViewableResource(observingCiv)
|
resource -> observingCiv != null && hasViewableResource(observingCiv)
|
||||||
"resource" -> observingCiv != null && hasViewableResource(observingCiv)
|
"resource" -> observingCiv != null && hasViewableResource(observingCiv)
|
||||||
"Water resource" -> isWater && observingCiv != null && hasViewableResource(observingCiv)
|
"Water resource" -> isWater && observingCiv != null && hasViewableResource(observingCiv)
|
||||||
"Natural Wonder" -> naturalWonder != null
|
|
||||||
"Featureless" -> terrainFeatures.isEmpty()
|
"Featureless" -> terrainFeatures.isEmpty()
|
||||||
Constants.freshWaterFilter -> isAdjacentTo(Constants.freshWater, observingCiv)
|
Constants.freshWaterFilter -> isAdjacentTo(Constants.freshWater, observingCiv)
|
||||||
|
|
||||||
in terrainFeatures -> true
|
|
||||||
else -> {
|
else -> {
|
||||||
if (terrainUniqueMap.containsFilteringUnique(filter)) return true
|
if (allTerrains.any { it.matchesFilter(filter) }) return true
|
||||||
if (getOwner()?.matchesFilter(filter) == true) return true
|
if (getOwner()?.matchesFilter(filter) == true) return true
|
||||||
|
|
||||||
// Resource type check is last - cannot succeed if no resource here
|
// Resource type check is last - cannot succeed if no resource here
|
||||||
|
@ -79,10 +79,9 @@ class TileInfoImprovementFunctions(val tile: Tile) {
|
|||||||
.any { civInfo.getResourceAmount(it.params[1]) < it.params[0].toInt() })
|
.any { civInfo.getResourceAmount(it.params[1]) < it.params[0].toInt() })
|
||||||
yield(ImprovementBuildingProblem.MissingResources)
|
yield(ImprovementBuildingProblem.MissingResources)
|
||||||
|
|
||||||
val knownFeatureRemovals = tile.ruleset.tileImprovements.values
|
val knownFeatureRemovals = tile.ruleset.tileRemovals
|
||||||
.filter { rulesetImprovement ->
|
.filter { rulesetImprovement ->
|
||||||
rulesetImprovement.name.startsWith(Constants.remove)
|
RoadStatus.values().none { it.removeAction == rulesetImprovement.name }
|
||||||
&& RoadStatus.values().none { it.removeAction == rulesetImprovement.name }
|
|
||||||
&& (rulesetImprovement.techRequired == null || civInfo.tech.isResearched(rulesetImprovement.techRequired!!))
|
&& (rulesetImprovement.techRequired == null || civInfo.tech.isResearched(rulesetImprovement.techRequired!!))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,16 +106,17 @@ class TileInfoImprovementFunctions(val tile: Tile) {
|
|||||||
): Boolean {
|
): Boolean {
|
||||||
val topTerrain = tile.lastTerrain
|
val topTerrain = tile.lastTerrain
|
||||||
// We can build if we are specifically allowed to build on this terrain
|
// We can build if we are specifically allowed to build on this terrain
|
||||||
if (isAllowedOnFeature(topTerrain.name)) return true
|
if (isAllowedOnFeature(topTerrain)) return true
|
||||||
|
|
||||||
// Otherwise, we can if this improvement removes the top terrain
|
// Otherwise, we can if this improvement removes the top terrain
|
||||||
if (!hasUnique(UniqueType.RemovesFeaturesIfBuilt, stateForConditionals)) return false
|
if (!hasUnique(UniqueType.RemovesFeaturesIfBuilt, stateForConditionals)) return false
|
||||||
val removeAction = tile.ruleset.tileImprovements[Constants.remove + topTerrain.name] ?: return false
|
if (knownFeatureRemovals == null) return false
|
||||||
// and we have the tech to remove that top terrain
|
val featureRemovals = tile.terrainFeatures.map {
|
||||||
if (removeAction.techRequired != null && (knownFeatureRemovals == null || removeAction !in knownFeatureRemovals)) return false
|
feature -> tile.ruleset.tileRemovals.firstOrNull{ it.name == Constants.remove + feature } }
|
||||||
// and we can build it on the tile without the top terrain
|
if (featureRemovals.any { it != null && it !in knownFeatureRemovals }) return false
|
||||||
val clonedTile = tile.clone()
|
val clonedTile = tile.clone()
|
||||||
clonedTile.removeTerrainFeature(topTerrain.name)
|
clonedTile.setTerrainFeatures(tile.terrainFeatures.filterNot {
|
||||||
|
feature -> featureRemovals.any{ it?.name?.removePrefix(Constants.remove) == feature } })
|
||||||
return clonedTile.improvementFunctions.canImprovementBeBuiltHere(improvement, resourceIsVisible, knownFeatureRemovals, stateForConditionals)
|
return clonedTile.improvementFunctions.canImprovementBeBuiltHere(improvement, resourceIsVisible, knownFeatureRemovals, stateForConditionals)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,7 +175,7 @@ class TileInfoImprovementFunctions(val tile: Tile) {
|
|||||||
// At this point we know this is a normal improvement and that there is no reason not to allow it to be built.
|
// At this point we know this is a normal improvement and that there is no reason not to allow it to be built.
|
||||||
|
|
||||||
// Lastly we check if the improvement may be built on this terrain or resource
|
// Lastly we check if the improvement may be built on this terrain or resource
|
||||||
improvement.canBeBuiltOn(tile.lastTerrain.name) -> true
|
improvement.isAllowedOnFeature(tile.lastTerrain) -> true
|
||||||
tile.isLand && improvement.canBeBuiltOn("Land") -> true
|
tile.isLand && improvement.canBeBuiltOn("Land") -> true
|
||||||
tile.isWater && improvement.canBeBuiltOn("Water") -> true
|
tile.isWater && improvement.canBeBuiltOn("Water") -> true
|
||||||
// DO NOT reverse this &&. isAdjacentToFreshwater() is a lazy which calls a function, and reversing it breaks the tests.
|
// DO NOT reverse this &&. isAdjacentToFreshwater() is a lazy which calls a function, and reversing it breaks the tests.
|
||||||
@ -213,14 +213,14 @@ class TileInfoImprovementFunctions(val tile: Tile) {
|
|||||||
if (improvementObject != null && improvementObject.hasUnique(UniqueType.RemovesFeaturesIfBuilt)) {
|
if (improvementObject != null && improvementObject.hasUnique(UniqueType.RemovesFeaturesIfBuilt)) {
|
||||||
// Remove terrainFeatures that a Worker can remove
|
// Remove terrainFeatures that a Worker can remove
|
||||||
// and that aren't explicitly allowed under the improvement
|
// and that aren't explicitly allowed under the improvement
|
||||||
val removableTerrainFeatures = tile.terrainFeatures.filter { feature ->
|
val removableTerrainFeatures = tile.terrainFeatureObjects.filter { feature ->
|
||||||
val removingAction = "${Constants.remove}$feature"
|
val removingAction = "${Constants.remove}${feature.name}"
|
||||||
|
|
||||||
removingAction in tile.ruleset.tileImprovements // is removable
|
removingAction in tile.ruleset.tileImprovements // is removable
|
||||||
&& !improvementObject.isAllowedOnFeature(feature) // cannot coexist
|
&& !improvementObject.isAllowedOnFeature(feature) // cannot coexist
|
||||||
}
|
}
|
||||||
|
|
||||||
tile.setTerrainFeatures(tile.terrainFeatures.filterNot { it in removableTerrainFeatures })
|
tile.setTerrainFeatures(tile.terrainFeatures.filterNot { feature -> removableTerrainFeatures.any { it.name == feature } })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (civToActivateBroaderEffects != null && improvementObject != null
|
if (civToActivateBroaderEffects != null && improvementObject != null
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package com.unciv.models.ruleset
|
package com.unciv.models.ruleset
|
||||||
|
|
||||||
import com.badlogic.gdx.files.FileHandle
|
import com.badlogic.gdx.files.FileHandle
|
||||||
|
import com.unciv.Constants
|
||||||
import com.unciv.json.fromJsonFile
|
import com.unciv.json.fromJsonFile
|
||||||
import com.unciv.json.json
|
import com.unciv.json.json
|
||||||
import com.unciv.logic.BackwardCompatibility.updateDeprecations
|
import com.unciv.logic.BackwardCompatibility.updateDeprecations
|
||||||
@ -77,6 +78,8 @@ class Ruleset {
|
|||||||
units.values.filter { it.hasUnique(UniqueType.GreatPersonFromCombat, StateForConditionals.IgnoreConditionals) }
|
units.values.filter { it.hasUnique(UniqueType.GreatPersonFromCombat, StateForConditionals.IgnoreConditionals) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val tileRemovals by lazy { tileImprovements.values.filter { it.name.startsWith(Constants.remove) } }
|
||||||
|
|
||||||
/** Contains all happiness levels that moving *from* them, to one *below* them, can change uniques that apply */
|
/** Contains all happiness levels that moving *from* them, to one *below* them, can change uniques that apply */
|
||||||
val allHappinessLevelsThatAffectUniques by lazy {
|
val allHappinessLevelsThatAffectUniques by lazy {
|
||||||
sequence {
|
sequence {
|
||||||
|
@ -2,6 +2,7 @@ package com.unciv.models.ruleset.tile
|
|||||||
|
|
||||||
import com.badlogic.gdx.graphics.Color
|
import com.badlogic.gdx.graphics.Color
|
||||||
import com.unciv.Constants
|
import com.unciv.Constants
|
||||||
|
import com.unciv.logic.MultiFilter
|
||||||
import com.unciv.models.ruleset.Belief
|
import com.unciv.models.ruleset.Belief
|
||||||
import com.unciv.models.ruleset.Ruleset
|
import com.unciv.models.ruleset.Ruleset
|
||||||
import com.unciv.models.ruleset.RulesetStatsObject
|
import com.unciv.models.ruleset.RulesetStatsObject
|
||||||
@ -143,6 +144,27 @@ class Terrain : RulesetStatsObject() {
|
|||||||
return textList
|
return textList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun matchesFilter(filter: String): Boolean {
|
||||||
|
return MultiFilter.multiFilter(filter, { matchesSingleFilter(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Implements [UniqueParameterType.TerrainFilter][com.unciv.models.ruleset.unique.UniqueParameterType.TerrainFilter] */
|
||||||
|
fun matchesSingleFilter(filter: String): Boolean {
|
||||||
|
return when (filter) {
|
||||||
|
in Constants.all -> true
|
||||||
|
name -> true
|
||||||
|
"Terrain" -> true
|
||||||
|
in Constants.all -> true
|
||||||
|
"Open terrain" -> !isRough()
|
||||||
|
"Rough terrain" -> isRough()
|
||||||
|
type.name -> true
|
||||||
|
"Natural Wonder" -> type == TerrainType.NaturalWonder
|
||||||
|
"Terrain Feature" -> type == TerrainType.TerrainFeature
|
||||||
|
|
||||||
|
else -> uniques.contains(filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setTransients() {
|
fun setTransients() {
|
||||||
damagePerTurn = getMatchingUniques(UniqueType.DamagesContainingUnits).sumOf { it.params[0].toInt() }
|
damagePerTurn = getMatchingUniques(UniqueType.DamagesContainingUnits).sumOf { it.params[0].toInt() }
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,9 @@ class TileImprovement : RulesetStatsObject() {
|
|||||||
fun canBeBuiltOn(terrain: String): Boolean {
|
fun canBeBuiltOn(terrain: String): Boolean {
|
||||||
return terrain in terrainsCanBeBuiltOn
|
return terrain in terrainsCanBeBuiltOn
|
||||||
}
|
}
|
||||||
|
fun canBeBuiltOn(terrain: Terrain): Boolean {
|
||||||
|
return terrainsCanBeBuiltOn.any{ terrain.matchesFilter(it) }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check: Is this improvement allowed on a [given][name] terrain feature?
|
* Check: Is this improvement allowed on a [given][name] terrain feature?
|
||||||
@ -86,7 +89,8 @@ class TileImprovement : RulesetStatsObject() {
|
|||||||
* so this check is done in conjunction - for the user, success means he does not need to remove
|
* so this check is done in conjunction - for the user, success means he does not need to remove
|
||||||
* a terrain feature, thus the unique name.
|
* a terrain feature, thus the unique name.
|
||||||
*/
|
*/
|
||||||
fun isAllowedOnFeature(name: String) = terrainsCanBeBuiltOn.contains(name) || getMatchingUniques(UniqueType.NoFeatureRemovalNeeded).any { it.params[0] == name }
|
fun isAllowedOnFeature(terrain: Terrain) = canBeBuiltOn(terrain)
|
||||||
|
|| getMatchingUniques(UniqueType.NoFeatureRemovalNeeded).any { terrain.matchesFilter(it.params[0]) }
|
||||||
|
|
||||||
/** Implements [UniqueParameterType.ImprovementFilter][com.unciv.models.ruleset.unique.UniqueParameterType.ImprovementFilter] */
|
/** Implements [UniqueParameterType.ImprovementFilter][com.unciv.models.ruleset.unique.UniqueParameterType.ImprovementFilter] */
|
||||||
fun matchesFilter(filter: String): Boolean {
|
fun matchesFilter(filter: String): Boolean {
|
||||||
|
@ -30,12 +30,11 @@ class TileImprovementConstructionTests {
|
|||||||
testGame.makeHexagonalMap(4)
|
testGame.makeHexagonalMap(4)
|
||||||
tileMap = testGame.tileMap
|
tileMap = testGame.tileMap
|
||||||
civInfo = testGame.addCiv()
|
civInfo = testGame.addCiv()
|
||||||
civInfo.tech.researchedTechnologies.addAll(testGame.ruleset.technologies.values)
|
for (tech in testGame.ruleset.technologies.values)
|
||||||
civInfo.tech.techsResearched.addAll(testGame.ruleset.technologies.keys)
|
civInfo.tech.addTechnology(tech.name)
|
||||||
city = testGame.addCity(civInfo, tileMap[0,0])
|
city = testGame.addCity(civInfo, tileMap[0,0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun allTerrainSpecificImprovementsCanBeBuilt() {
|
fun allTerrainSpecificImprovementsCanBeBuilt() {
|
||||||
for (improvement in testGame.ruleset.tileImprovements.values) {
|
for (improvement in testGame.ruleset.tileImprovements.values) {
|
||||||
@ -214,6 +213,39 @@ class TileImprovementConstructionTests {
|
|||||||
assert(tile.improvement == "Camp") // Camp can be both on Forest AND on Plains, so not removed
|
assert(tile.improvement == "Camp") // Camp can be both on Forest AND on Plains, so not removed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun improvementCannotBuildWhenNotAllowed() {
|
||||||
|
val tile = tileMap[1,1]
|
||||||
|
tile.baseTerrain ="Grassland"
|
||||||
|
tile.addTerrainFeature("Forest")
|
||||||
|
|
||||||
|
val improvement = testGame.createTileImprovement()
|
||||||
|
Assert.assertFalse("Forest doesn't allow building unless allowed",
|
||||||
|
tile.improvementFunctions.canBuildImprovement(improvement, civInfo))
|
||||||
|
|
||||||
|
|
||||||
|
val allowedImprovement = testGame.createTileImprovement()
|
||||||
|
allowedImprovement.terrainsCanBeBuiltOn += "Forest"
|
||||||
|
Assert.assertTrue("Forest should allow building when allowed",
|
||||||
|
tile.improvementFunctions.canBuildImprovement(allowedImprovement, civInfo))
|
||||||
|
tile.changeImprovement(allowedImprovement.name)
|
||||||
|
Assert.assertTrue(tile.improvement == allowedImprovement.name)
|
||||||
|
Assert.assertTrue("Forest should not be removed with this improvement", tile.terrainFeatures.contains("Forest"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun improvementDoesntNeedRemovalCanBuildHere() {
|
||||||
|
val tile = tileMap[1,1]
|
||||||
|
tile.baseTerrain ="Grassland"
|
||||||
|
tile.addTerrainFeature("Forest")
|
||||||
|
|
||||||
|
val improvement = testGame.createTileImprovement("Does not need removal of [Forest]")
|
||||||
|
Assert.assertTrue(tile.improvementFunctions.canBuildImprovement(improvement, civInfo))
|
||||||
|
tile.changeImprovement(improvement.name)
|
||||||
|
Assert.assertTrue(tile.improvement == improvement.name)
|
||||||
|
Assert.assertTrue("Forest should not be removed with this improvement", tile.terrainFeatures.contains("Forest"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun statsDiffFromRemovingForestTakesRemovedLumberMillIntoAccount() {
|
fun statsDiffFromRemovingForestTakesRemovedLumberMillIntoAccount() {
|
||||||
val tile = tileMap[1,1]
|
val tile = tileMap[1,1]
|
||||||
|
@ -116,6 +116,7 @@ class GlobalUniquesTests {
|
|||||||
city.cityStats.update()
|
city.cityStats.update()
|
||||||
Assert.assertTrue(city.cityStats.finalStatList["Buildings"]!!.gold == 3f)
|
Assert.assertTrue(city.cityStats.finalStatList["Buildings"]!!.gold == 3f)
|
||||||
tile.baseTerrain = Constants.grassland
|
tile.baseTerrain = Constants.grassland
|
||||||
|
tile.setTransients()
|
||||||
city.cityStats.update()
|
city.cityStats.update()
|
||||||
Assert.assertTrue(city.cityStats.finalStatList["Buildings"]!!.gold == 0f)
|
Assert.assertTrue(city.cityStats.finalStatList["Buildings"]!!.gold == 0f)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user