Improved clarity and moddability of building improvements (#6712)

* Renamed tile.hasUnique, deprecated `Indestructable`, unique for citadels
Also refactored the consumption of (great) people out of UnitActions.

* Reworked when improvements can be build somewhere for more clarity

* Made resources improvable by multiple improvements; Offshore Platform

* Fix compatability

* WIP

* Fixed the tests, but better

* I suppose I might as well update this now that we're a version later
This commit is contained in:
Xander Lenstra
2022-05-11 00:35:21 +02:00
committed by GitHub
parent 7b4833741d
commit 8fcfbf752b
33 changed files with 1187 additions and 1042 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -44,22 +44,27 @@
"name": "Camp",
"turnsToBuild": 7,
"techRequired": "Trapping",
"uniques": ["Does not need removal of [Forest]","Does not need removal of [Jungle]","[+1 Gold] <after discovering [Economics]>"],
"uniques": ["Does not need removal of [Forest]","Does not need removal of [Jungle]","[+1 Gold] <after discovering [Economics]>", "Can only be built to improve a resource"],
"shortcutKey": "C"
},
{
"name": "Oil well",
"terrainsCanBeBuiltOn": ["Coast"],
"turnsToBuild": 9,
"techRequired": "Biology",
"uniques": ["Cannot be built on [Coast] tiles <before discovering [Refrigeration]>"],
"uniques": ["Can only be built to improve a resource", "Cannot be built on [Water] tiles"],
"shortcutKey": "W"
},
{
"name": "Offshore Platform",
"techRequired": "Refrigeration",
"uniques": ["Can only be built to improve a resource", "Can only be built on [Water] tiles"],
"shortcutKey": "P"
},
{
"name": "Pasture",
"turnsToBuild": 8,
"techRequired": "Animal Husbandry",
"uniques": ["[+1 Food] <after discovering [Fertilizer]>"],
"uniques": ["[+1 Food] <after discovering [Fertilizer]>", "Can only be built to improve a resource"],
"shortcutKey": "P"
},
{
@ -67,14 +72,14 @@
"turnsToBuild": 6,
"gold": 1,
"techRequired": "Calendar",
"uniques": ["[+1 Food] <after discovering [Fertilizer]>"],
"uniques": ["[+1 Food] <after discovering [Fertilizer]>", "Can only be built to improve a resource"],
"shortcutKey": "P"
},
{
"name": "Quarry",
"turnsToBuild": 8,
"techRequired": "Masonry",
"uniques": ["[+1 Production] <after discovering [Chemistry]>"],
"uniques": ["[+1 Production] <after discovering [Chemistry]>", "Can only be built to improve a resource"],
"shortcutKey": "Q"
},
{
@ -82,7 +87,8 @@
"terrainsCanBeBuiltOn": ["Coast"],
"food": 1,
"techRequired": "Sailing",
"uniques": ["[+1 Gold] <after discovering [Compass]>"]
"uniques": ["[+1 Gold] <after discovering [Compass]>", "Can only be built to improve a resource"],
"shortcutKey": "F"
},
// Military improvement
@ -98,6 +104,7 @@
// Transportation
{
"name": "Road",
"terrainsCanBeBuiltOn": ["Land"],
"turnsToBuild": 4,
"techRequired": "The Wheel",
// "Costs [1] gold per turn when in your territory" does nothing and is only to inform the user
@ -111,6 +118,7 @@
},
{
"name": "Railroad",
"terrainsCanBeBuiltOn": ["Land"],
"turnsToBuild": 4,
"techRequired": "Railroads",
"uniques": ["Can be built outside your borders", "Costs [2] gold per turn when in your territory"],
@ -119,10 +127,11 @@
},
// Removals
// Any improvement that starts with 'Remove ' is automatically changed into
// the improvement that removes the terrainfeature after it.
{
"name": "Remove Forest",
"turnsToBuild": 4,
"terrainsCanBeBuiltOn": ["Forest"],
"techRequired": "Mining",
"uniques": ["Can be built outside your borders"],
"shortcutKey": "X",
@ -131,7 +140,6 @@
{
"name": "Remove Jungle",
"turnsToBuild": 7,
"terrainsCanBeBuiltOn": ["Jungle"],
"techRequired": "Bronze Working",
"uniques": ["Can be built outside your borders"],
"shortcutKey": "X"
@ -139,15 +147,13 @@
{
"name": "Remove Fallout",
"turnsToBuild": 2,
"terrainsCanBeBuiltOn": ["Fallout"],
// Has no tile improvements as it can always be built
// Has no tech requirements as it can always be built
"uniques": ["Can be built outside your borders"],
"shortcutKey": "X"
},
{
"name": "Remove Marsh",
"turnsToBuild": 6,
"terrainsCanBeBuiltOn": ["Marsh"],
"techRequired": "Masonry",
"uniques": ["Can be built outside your borders"],
"shortcutKey": "X"
@ -173,43 +179,51 @@
// Great Person improvements
{
"name": "Academy",
"terrainsCanBeBuiltOn": ["Land"],
"science": 8,
"uniques": ["Great Improvement", "[+2 Science] <after discovering [Scientific Theory]>", "[+2 Science] <after discovering [Atomic Theory]>"]
},
{
"name": "Landmark",
"terrainsCanBeBuiltOn": ["Land"],
"culture": 6,
"uniques": ["Great Improvement"]
},
{
"name": "Manufactory",
"terrainsCanBeBuiltOn": ["Land"],
"production": 4,
"uniques": ["Great Improvement", "[+1 Production] <after discovering [Chemistry]>"]
},
{
"name": "Customs house",
"terrainsCanBeBuiltOn": ["Land"],
"gold": 4,
"uniques": ["Great Improvement", "[+1 Gold] <after discovering [Economics]>"]
},
{
"name": "Holy site",
"terrainsCanBeBuiltOn": ["Land"],
"faith": 6,
"uniques": ["Great Improvement"]
},
{
"name": "Citadel",
"terrainsCanBeBuiltOn": ["Land"],
"uniques": [
"Great Improvement",
"Gives a defensive bonus of [100]%",
"Adjacent enemy units ending their turn take [30] damage",
"Can be built just outside your borders",
"Constructing it will take over the tiles around it and assign them to your closest city"]
"Constructing it will take over the tiles around it and assign them to your closest city"
]
},
//Civilization unique improvements
{
"name": "Moai",
"uniqueTo": "Polynesia",
"terrainsCanBeBuiltOn": ["Land"],
"culture": 1,
"turnsToBuild": 4,
"uniques": ["Can only be built on [Coastal] tiles", "[+1 Culture] for each adjacent [Moai]", "[+1 Gold] <after discovering [Flight]>"],
@ -240,26 +254,31 @@
"shortcutKey": "F"
},
// Unbuildable improvements
{
"name": "Ancient ruins",
"uniques": ["Unpillagable", "Provides a random bonus when entered"]
"name": "Ancient ruins",
"terrainsCanBeBuiltOn": ["Land"],
"uniques": ["Unpillagable", "Provides a random bonus when entered", "Unbuildable"]
},
{
"name": "City ruins",
"uniques": ["Unpillagable"],
"name": "City ruins",
"terrainsCanBeBuiltOn": ["Land"],
"uniques": ["Unpillagable", "Unbuildable"],
"civilopediaText": [{"text":"A bleak reminder of the destruction wreaked by War"}]
},
{
"name": "City center",
"uniques": ["Unpillagable", "Indestructible"],
"name": "City center",
"terrainsCanBeBuiltOn": ["Land"],
"uniques": ["Unpillagable", "Irremovable", "Unbuildable"],
"civilopediaText": [
{"text":"Marks the center of a city"},
{"text":"Appearance changes with the technological era of the owning civilization"}
]
},
{
"name": "Barbarian encampment",
"uniques": ["Unpillagable"],
"name": "Barbarian encampment",
"terrainsCanBeBuiltOn": ["Land"],
"uniques": ["Unpillagable", "Unbuildable"],
"civilopediaText": [{"text":"Home to uncivilized barbarians, will spawn a hostile unit from time to time"}]
}
]

View File

@ -157,7 +157,7 @@
"revealedBy": "Biology",
"terrainsCanBeFoundOn": ["Desert","Coast","Tundra","Snow","Marsh","Jungle"],
"production": 1,
"improvement": "Oil well",
"improvedBy": ["Oil well", "Offshore Platform"],
"improvementStats": {"production": 3},
"uniques": ["Deposits in [Coast] tiles always provide [4] resources",
"Guaranteed with Strategic Balance resource option",

View File

@ -44,22 +44,27 @@
"name": "Camp",
"turnsToBuild": 7,
"techRequired": "Trapping",
"uniques": ["Does not need removal of [Forest]","Does not need removal of [Jungle]","[+1 Gold] <after discovering [Economics]>"],
"uniques": ["Does not need removal of [Forest]","Does not need removal of [Jungle]","[+1 Gold] <after discovering [Economics]>", "Can only be built to improve a resource"],
"shortcutKey": "C"
},
{
"name": "Oil well",
"terrainsCanBeBuiltOn": ["Coast"],
"turnsToBuild": 9,
"techRequired": "Biology",
"uniques": ["Cannot be built on [Coast] tiles <before discovering [Refrigeration]>"],
"uniques": ["Can only be built to improve a resource", "Cannot be built on [Water] tiles"],
"shortcutKey": "W"
},
{
"name": "Offshore Platform",
"techRequired": "Refrigeration",
"uniques": ["Can only be built to improve a resource", "Can only be built on [Water] tiles"],
"shortcutKey": "P"
},
{
"name": "Pasture",
"turnsToBuild": 8,
"techRequired": "Animal Husbandry",
"uniques": ["[+1 Food] <after discovering [Fertilizer]>"],
"uniques": ["[+1 Food] <after discovering [Fertilizer]>", "Can only be built to improve a resource"],
"shortcutKey": "P"
},
{
@ -67,14 +72,14 @@
"turnsToBuild": 6,
"gold": 1,
"techRequired": "Calendar",
"uniques": ["[+1 Food] <after discovering [Fertilizer]>"],
"uniques": ["[+1 Food] <after discovering [Fertilizer]>", "Can only be built to improve a resource"],
"shortcutKey": "P"
},
{
"name": "Quarry",
"turnsToBuild": 8,
"techRequired": "Masonry",
"uniques": ["[+1 Production] <after discovering [Chemistry]>"],
"uniques": ["[+1 Production] <after discovering [Chemistry]>", "Can only be built to improve a resource"],
"shortcutKey": "Q"
},
{
@ -82,7 +87,8 @@
"terrainsCanBeBuiltOn": ["Coast"],
"food": 1,
"techRequired": "Sailing",
"uniques": ["[+1 Gold] <after discovering [Compass]>"]
"uniques": ["[+1 Gold] <after discovering [Compass]>", "Can only be built to improve a resource"],
"shortcutKey": "F"
},
// Military improvement
@ -98,6 +104,7 @@
// Transportation
{
"name": "Road",
"terrainsCanBeBuiltOn": ["Land"],
"turnsToBuild": 4,
"techRequired": "The Wheel",
// "Costs [1] gold per turn when in your territory" does nothing and is only to inform the user
@ -111,6 +118,7 @@
},
{
"name": "Railroad",
"terrainsCanBeBuiltOn": ["Land"],
"turnsToBuild": 4,
"techRequired": "Railroads",
"uniques": ["Can be built outside your borders", "Costs [2] gold per turn when in your territory"],
@ -119,10 +127,11 @@
},
// Removals
// Any improvement that starts with 'Remove ' is automatically changed into
// the improvement that removes the terrainfeature after it.
{
"name": "Remove Forest",
"turnsToBuild": 4,
"terrainsCanBeBuiltOn": ["Forest"],
"techRequired": "Mining",
"uniques": ["Can be built outside your borders"],
"shortcutKey": "X",
@ -131,7 +140,6 @@
{
"name": "Remove Jungle",
"turnsToBuild": 7,
"terrainsCanBeBuiltOn": ["Jungle"],
"techRequired": "Bronze Working",
"uniques": ["Can be built outside your borders"],
"shortcutKey": "X"
@ -139,15 +147,13 @@
{
"name": "Remove Fallout",
"turnsToBuild": 2,
"terrainsCanBeBuiltOn": ["Fallout"],
// Has no tile improvements as it can always be built
// Has no tech requirements as it can always be built
"uniques": ["Can be built outside your borders"],
"shortcutKey": "X"
},
{
"name": "Remove Marsh",
"turnsToBuild": 6,
"terrainsCanBeBuiltOn": ["Marsh"],
"techRequired": "Masonry",
"uniques": ["Can be built outside your borders"],
"shortcutKey": "X"
@ -173,43 +179,51 @@
// Great Person improvements
{
"name": "Academy",
"terrainsCanBeBuiltOn": ["Land"],
"science": 8,
"uniques": ["Great Improvement", "[+2 Science] <after discovering [Scientific Theory]>"]
},
{
"name": "Landmark",
"terrainsCanBeBuiltOn": ["Land"],
"culture": 6,
"uniques": ["Great Improvement"]
},
{
"name": "Manufactory",
"terrainsCanBeBuiltOn": ["Land"],
"production": 4,
"uniques": ["Great Improvement", "[+1 Production] <after discovering [Chemistry]>"]
},
{
"name": "Customs house",
"terrainsCanBeBuiltOn": ["Land"],
"gold": 4,
"uniques": ["Great Improvement", "[+1 Gold] <after discovering [Economics]>"]
},
{
"name": "Holy site",
"terrainsCanBeBuiltOn": ["Land"],
"faith": 6,
"uniques": ["Great Improvement"]
},
{
"name": "Citadel",
"terrainsCanBeBuiltOn": ["Land"],
"uniques": [
"Great Improvement",
"Gives a defensive bonus of [100]%",
"Adjacent enemy units ending their turn take [30] damage",
"Can be built just outside your borders",
"Constructing it will take over the tiles around it and assign them to your closest city"]
"Constructing it will take over the tiles around it and assign them to your closest city"
]
},
//Civilization unique improvements
{
"name": "Moai",
"uniqueTo": "Polynesia",
"terrainsCanBeBuiltOn": ["Land"],
"culture": 1,
"turnsToBuild": 4,
"uniques": ["Can only be built on [Coastal] tiles", "[+1 Culture] for each adjacent [Moai]", "[+1 Gold] <after discovering [Flight]>"],
@ -230,26 +244,31 @@
"shortcutKey": "F"
},
// Unbuildable improvements
{
"name": "Ancient ruins",
"uniques": ["Unpillagable", "Provides a random bonus when entered"]
"name": "Ancient ruins",
"terrainsCanBeBuiltOn": ["Land"],
"uniques": ["Unpillagable", "Provides a random bonus when entered", "Unbuildable"]
},
{
"name": "City ruins",
"uniques": ["Unpillagable"],
"name": "City ruins",
"terrainsCanBeBuiltOn": ["Land"],
"uniques": ["Unpillagable", "Unbuildable"],
"civilopediaText": [{"text":"A bleak reminder of the destruction wreaked by War"}]
},
{
"name": "City center",
"uniques": ["Unpillagable", "Indestructible"],
"name": "City center",
"terrainsCanBeBuiltOn": ["Land"],
"uniques": ["Unpillagable", "Irremovable", "Unbuildable"],
"civilopediaText": [
{"text":"Marks the center of a city"},
{"text":"Appearance changes with the technological era of the owning civilization"}
]
},
{
"name": "Barbarian encampment",
"uniques": ["Unpillagable"],
"name": "Barbarian encampment",
"terrainsCanBeBuiltOn": ["Land"],
"uniques": ["Unpillagable", "Unbuildable"],
"civilopediaText": [{"text":"Home to uncivilized barbarians, will spawn a hostile unit from time to time"}]
}
]

View File

@ -160,7 +160,7 @@
"revealedBy": "Biology",
"terrainsCanBeFoundOn": ["Desert","Coast","Tundra","Snow","Marsh","Jungle"],
"production": 1,
"improvement": "Oil well",
"improvedBy": ["Oil well", "Offshore Platform"],
"improvementStats": {"production": 3},
"uniques": ["Deposits in [Coast] tiles always provide [4] resources",
"Guaranteed with Strategic Balance resource option",

View File

@ -30,6 +30,7 @@ object Constants {
val vegetation = arrayOf(forest, jungle)
// Note the difference in case. **Not** interchangeable!
// TODO this is very opaque behaviour to modders
/** The "Fresh water" terrain _unique_ */
const val freshWater = "Fresh water"
/** The "Fresh Water" terrain _filter_ */

View File

@ -236,7 +236,7 @@ class Encampment() {
|| it.isCityCenter()
|| it.getFirstUnit() != null
|| (it.isWater && !canSpawnBoats)
|| (it.hasUnique(UniqueType.FreshWater) && it.isWater) // No Lakes
|| (it.terrainHasUnique(UniqueType.FreshWater) && it.isWater) // No Lakes
}
if (validTiles.isEmpty()) return false

View File

@ -162,10 +162,12 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
}
for (i in 1..10) bfs.nextStep()
if (!bfs.getReachedTiles()
.any { it.hasViewableResource(civInfo) && it.improvement == null && it.getOwner() == civInfo
&& it.tileResource.improvement != null
&& it.canBuildImprovement(it.ruleset.tileImprovements[it.tileResource.improvement]!!, civInfo)
.any { tile ->
tile.hasViewableResource(civInfo) && tile.improvement == null && tile.getOwner() == civInfo
&& tile.tileResource.getImprovements().any {
tile.canBuildImprovement(tile.ruleset.tileImprovements[it]!!, civInfo)
}
}
) return
addChoice(

View File

@ -38,8 +38,8 @@ object SpecificUnitAutomation {
unit.movement.headTowards(closestReachableResource)
// could be either fishing boats or oil well
val improvement = closestReachableResource.tileResource.improvement
if (unit.currentTile == closestReachableResource && improvement != null)
val isImprovable = closestReachableResource.tileResource.getImprovements().any()
if (isImprovable && unit.currentTile == closestReachableResource)
UnitActions.getWaterImprovementAction(unit)?.action?.invoke()
}
}

View File

@ -299,7 +299,7 @@ class WorkerAutomation(
if (chosenImprovement != null && tile.canBuildImprovement(chosenImprovement, civInfo) && unit.canBuildImprovement(chosenImprovement, tile)) return true
} else if (!tile.containsGreatImprovement() && tile.hasViewableResource(civInfo)
&& tile.tileResource.improvement != tile.improvement
&& tile.tileResource.isImprovedBy(tile.improvement!!)
&& (chooseImprovement(unit, tile) // if the chosen improvement is not null and buildable
.let { it != null && tile.canBuildImprovement(it, civInfo) && unit.canBuildImprovement(it, tile)}))
return true
@ -354,17 +354,14 @@ class WorkerAutomation(
tile.terrainFeatures.isNotEmpty()
&& isUnbuildableAndRemovable(lastTerrain)
&& !isResourceImprovementAllowedOnFeature(tile) -> Constants.remove + lastTerrain.name
else -> tile.tileResource.improvement
else -> tile.tileResource.getImprovements().firstOrNull { it in potentialTileImprovements }
}
val improvementString = when {
tile.improvementInProgress != null -> tile.improvementInProgress!!
improvementStringForResource != null -> {
if (potentialTileImprovements.containsKey(improvementStringForResource))
improvementStringForResource
// if this is a resource that HAS an improvement, but this unit can't build it, don't waste your time
else return null
}
improvementStringForResource != null -> improvementStringForResource
// if this is a resource that HAS an improvement, but this unit can't build it, don't waste your time
tile.resource != null && tile.tileResource.getImprovements().any() -> return null
tile.containsGreatImprovement() -> return null
tile.containsUnfinishedGreatImprovement() -> return null
@ -392,11 +389,10 @@ class WorkerAutomation(
* Assumes the caller ensured that terrainFeature and resource are both present!
*/
private fun isResourceImprovementAllowedOnFeature(tile: TileInfo): Boolean {
val resourceImprovementName = tile.tileResource.improvement
?: return false
val resourceImprovement = ruleSet.tileImprovements[resourceImprovementName]
?: return false
return tile.terrainFeatures.any { resourceImprovement.isAllowedOnFeature(it) }
return tile.tileResource.getImprovements().any { resourceImprovementName ->
val resourceImprovement = ruleSet.tileImprovements[resourceImprovementName] ?: return false
tile.terrainFeatures.any { resourceImprovement.isAllowedOnFeature(it) }
}
}
/**

View File

@ -732,7 +732,7 @@ object Battle {
}
// Pillage improvements, remove roads, add fallout
if (tile.improvement != null && !tile.getTileImprovement()!!.hasUnique(UniqueType.Indestructible)) {
if (tile.improvement != null && !tile.getTileImprovement()!!.hasUnique(UniqueType.Irremovable)) {
if (tile.getTileImprovement()!!.hasUnique(UniqueType.Unpillagable)) {
tile.improvement = null
} else {
@ -741,29 +741,28 @@ object Battle {
}
tile.roadStatus = RoadStatus.None
if (tile.isLand && !tile.isImpassible() && !tile.isCityCenter()) {
if (tile.hasUnique(UniqueType.DestroyableByNukesChance)) {
if (tile.terrainHasUnique(UniqueType.DestroyableByNukesChance)) {
for (terrainFeature in tile.terrainFeatureObjects) {
for (unique in terrainFeature.getMatchingUniques(UniqueType.DestroyableByNukesChance)) {
if (Random().nextFloat() >= unique.params[0].toFloat() / 100f) continue
tile.removeTerrainFeature(terrainFeature.name)
if (!tile.terrainFeatures.contains("Fallout") && !tile.hasUnique(UniqueType.Indestructible))
if (!tile.terrainFeatures.contains("Fallout"))
tile.addTerrainFeature("Fallout")
}
}
} else if (Random().nextFloat() < 0.5f && !tile.terrainFeatures.contains("Fallout") && !tile.hasUnique(UniqueType.Indestructible)) {
} else if (Random().nextFloat() < 0.5f && !tile.terrainFeatures.contains("Fallout")) {
tile.addTerrainFeature("Fallout")
}
if (!tile.hasUnique(UniqueType.DestroyableByNukes)) return
if (!tile.terrainHasUnique(UniqueType.DestroyableByNukes)) return
// Deprecated as of 3.19.19 -- If removed, the two successive `if`s above should be merged
val destructionChance = if (tile.hasUnique(UniqueType.ResistsNukes)) 0.25f
val destructionChance = if (tile.terrainHasUnique(UniqueType.ResistsNukes)) 0.25f
else 0.5f
if (Random().nextFloat() < destructionChance) {
for (terrainFeature in tile.terrainFeatureObjects)
if (terrainFeature.hasUnique(UniqueType.DestroyableByNukes))
tile.removeTerrainFeature(terrainFeature.name)
if (!tile.hasUnique(UniqueType.Indestructible))
tile.addTerrainFeature("Fallout")
tile.addTerrainFeature("Fallout")
}
//
}

View File

@ -410,12 +410,14 @@ class CityInfo {
if (resource.revealedBy != null && !civInfo.tech.isResearched(resource.revealedBy!!)) return 0
// Even if the improvement exists (we conquered an enemy city or somesuch) or we have a city on it, we won't get the resource until the correct tech is researched
if (resource.improvement != null) {
val improvement = getRuleset().tileImprovements[resource.improvement!!]!!
if (improvement.techRequired != null && !civInfo.tech.isResearched(improvement.techRequired!!)) return 0
if (resource.getImprovements().any()) {
if (!resource.getImprovements().any { improvementString ->
val improvement = getRuleset().tileImprovements[improvementString]!!
improvement.techRequired == null || civInfo.tech.isResearched(improvement.techRequired!!)
}) return 0
}
if (resource.improvement == tileInfo.improvement || tileInfo.isCityCenter()
if ((tileInfo.improvement != null && resource.isImprovedBy(tileInfo.improvement!!)) || tileInfo.isCityCenter()
// Per https://gaming.stackexchange.com/questions/53155/do-manufactories-and-customs-houses-sacrifice-the-strategic-or-luxury-resources
|| resource.resourceType == ResourceType.Strategic && tileInfo.containsGreatImprovement()
) {

View File

@ -378,7 +378,7 @@ class CityStats(val cityInfo: CityInfo) {
|| cityInfo.isWorked(it)
|| it.owningCity == cityInfo && (it.getTileImprovement()
?.hasUnique(UniqueType.TileProvidesYieldWithoutPopulation) == true
|| it.hasUnique(UniqueType.TileProvidesYieldWithoutPopulation))
|| it.terrainHasUnique(UniqueType.TileProvidesYieldWithoutPopulation))
})
stats.add(cell.getTileStats(cityInfo, cityInfo.civInfo, localUniqueCache))
statsFromTiles = stats

View File

@ -122,7 +122,7 @@ class CivInfoTransientUpdater(val civInfo: CivilizationInfo) {
var goldGained = 0
val discoveredNaturalWonders = civInfo.gameInfo.civilizations.filter { it != civInfo && it.isMajorCiv() }
.flatMap { it.naturalWonders }
if (tile.hasUnique(UniqueType.GrantsGoldToFirstToDiscover)
if (tile.terrainHasUnique(UniqueType.GrantsGoldToFirstToDiscover)
&& !discoveredNaturalWonders.contains(tile.naturalWonder!!)) {
goldGained += 500
}

View File

@ -20,8 +20,10 @@ import com.unciv.models.ruleset.tile.TileImprovement
import com.unciv.models.ruleset.unique.*
import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.ruleset.unit.UnitType
import com.unciv.models.stats.Stats
import com.unciv.ui.utils.filterAndLogic
import com.unciv.ui.utils.toPercent
import com.unciv.ui.worldscreen.unit.UnitActions
import java.text.DecimalFormat
import kotlin.math.pow
@ -661,10 +663,11 @@ class MapUnit {
tile.improvementInProgress == RoadStatus.Road.name -> tile.roadStatus = RoadStatus.Road
tile.improvementInProgress == RoadStatus.Railroad.name -> tile.roadStatus = RoadStatus.Railroad
else -> {
tile.improvement = tile.improvementInProgress
if (tile.resource != null) civInfo.updateDetailedCivResources()
val improvement = civInfo.gameInfo.ruleSet.tileImprovements[tile.improvementInProgress]!!
improvement.handleImprovementCompletion(this)
}
}
tile.improvementInProgress = null
}
@ -851,6 +854,30 @@ class MapUnit {
assignOwner(recipient)
recipient.updateViewableTiles()
}
/** Destroys the unit and gives stats if its a great person */
fun consume() {
addStatsPerGreatPersonUsage()
destroy()
}
private fun addStatsPerGreatPersonUsage() {
if (!isGreatPerson()) return
val gainedStats = Stats()
for (unique in civInfo.getMatchingUniques(UniqueType.ProvidesGoldWheneverGreatPersonExpended)) {
gainedStats.gold += (100 * civInfo.gameInfo.gameParameters.gameSpeed.modifier).toInt()
}
for (unique in civInfo.getMatchingUniques(UniqueType.ProvidesStatsWheneverGreatPersonExpended)) {
gainedStats.add(unique.stats)
}
if (gainedStats.isEmpty()) return
for (stat in gainedStats)
civInfo.addStat(stat.key, stat.value.toInt())
civInfo.addNotification("By expending your [$name] you gained [${gainedStats}]!", getTile().position, name)
}
fun removeFromTile() = currentTile.removeUnit(this)

View File

@ -243,11 +243,19 @@ open class TileInfo {
fun isRoughTerrain() = getAllTerrains().any{ it.isRough() }
fun hasUnique(uniqueType: UniqueType) = getAllTerrains().any { it.hasUnique(uniqueType) }
fun getMatchingUniques(uniqueType: UniqueType, stateForConditionals: StateForConditionals = StateForConditionals(tile=this) ): Sequence<Unique> {
/** Checks whether any of the TERRAINS of this tile has a certain unqiue */
fun terrainHasUnique(uniqueType: UniqueType) = getAllTerrains().any { it.hasUnique(uniqueType) }
/** Get all uniques of this type that any TERRAIN on this tile has */
fun getTerrainMatchingUniques(uniqueType: UniqueType, stateForConditionals: StateForConditionals = StateForConditionals(tile=this) ): Sequence<Unique> {
return getAllTerrains().flatMap { it.getMatchingUniques(uniqueType, stateForConditionals) }
}
/** Get all uniques of this type that any part of this tile has: terrains, improvement, resource */
fun getMatchingUniques(uniqueType: UniqueType, stateForConditionals: StateForConditionals = StateForConditionals(tile=this)) =
getTerrainMatchingUniques(uniqueType, stateForConditionals) +
(getTileImprovement()?.getMatchingUniques(uniqueType, stateForConditionals) ?: sequenceOf()) +
if (resource == null) sequenceOf() else tileResource.getMatchingUniques(uniqueType, stateForConditionals)
fun getWorkingCity(): CityInfo? {
val civInfo = getOwner() ?: return null
return civInfo.cities.firstOrNull { it.isWorked(this) }
@ -256,7 +264,7 @@ open class TileInfo {
fun isWorked(): Boolean = getWorkingCity() != null
fun providesYield() = getCity() != null && (isCityCenter() || isWorked()
|| getTileImprovement()?.hasUnique(UniqueType.TileProvidesYieldWithoutPopulation) == true
|| hasUnique(UniqueType.TileProvidesYieldWithoutPopulation))
|| terrainHasUnique(UniqueType.TileProvidesYieldWithoutPopulation))
fun isLocked(): Boolean {
val workingCity = getWorkingCity()
@ -405,7 +413,7 @@ open class TileInfo {
fun getImprovementStats(improvement: TileImprovement, observingCiv: CivilizationInfo, city: CityInfo?): Stats {
val stats = improvement.cloneStats()
if (hasViewableResource(observingCiv) && tileResource.improvement == improvement.name
if (hasViewableResource(observingCiv) && tileResource.isImprovedBy(improvement.name)
&& tileResource.improvementStats != null
)
stats.add(tileResource.improvementStats!!.clone()) // resource-specific improvement
@ -463,40 +471,6 @@ open class TileInfo {
return stats
}
/** Returns true if the [improvement] can be built on this [TileInfo] */
fun canBuildImprovement(improvement: TileImprovement, civInfo: CivilizationInfo): Boolean {
return when {
improvement.uniqueTo != null && improvement.uniqueTo != civInfo.civName -> false
improvement.techRequired != null && !civInfo.tech.isResearched(improvement.techRequired!!) -> false
getOwner() != civInfo && !(
improvement.hasUnique(UniqueType.CanBuildOutsideBorders)
|| ( // citadel can be built only next to or within own borders
improvement.hasUnique(UniqueType.CanBuildJustOutsideBorders)
&& neighbors.any { it.getOwner() == civInfo }
)
) -> false
improvement.getMatchingUniques(UniqueType.OnlyAvailableWhen, StateForConditionals.IgnoreConditionals).any {
!it.conditionalsApply(StateForConditionals(civInfo, tile=this))
} -> false
improvement.getMatchingUniques(UniqueType.ObsoleteWith).any {
civInfo.tech.isResearched(it.params[0])
} -> return false
improvement.getMatchingUniques(UniqueType.CannotBuildOnTile, StateForConditionals(civInfo=civInfo, tile=this)).any {
matchesTerrainFilter(it.params[0], civInfo)
} -> false
improvement.getMatchingUniques(UniqueType.ConsumesResources).any {
civInfo.getCivResourcesByName()[it.params[1]]!! < it.params[0].toInt()
} -> false
// Calling this function does double the check for 'cannot be build on tile', but this is unavoidable.
// Only in this function do we have the civInfo of the civ, so only here we can check whether
// conditionals apply. Additionally, the function below is also called when determining if
// an improvement can be on the tile in the given ruleset, in which case we do want to
// assume that all conditionals apply, which is done automatically when we don't include
// any state for conditionals. Therefore, duplicating the check is the easiest option.
else -> canImprovementBeBuiltHere(improvement, hasViewableResource(civInfo))
}
}
// This should be the only adjacency function
fun isAdjacentTo(terrainFilter:String): Boolean {
// Rivers are odd, as they aren't technically part of any specific tile but still count towards adjacency
@ -505,47 +479,103 @@ open class TileInfo {
return (neighbors + this).any { neighbor -> neighbor.matchesFilter(terrainFilter) }
}
/** Returns true if the [improvement] can be built on this [TileInfo] */
fun canBuildImprovement(improvement: TileImprovement, civInfo: CivilizationInfo): Boolean {
val stateForConditionals = StateForConditionals(civInfo, tile=this)
return when {
improvement.uniqueTo != null && improvement.uniqueTo != civInfo.civName -> false
improvement.techRequired != null && !civInfo.tech.isResearched(improvement.techRequired!!) -> false
getOwner() != civInfo && !(
improvement.hasUnique(UniqueType.CanBuildOutsideBorders, stateForConditionals)
|| ( // citadel can be built only next to or within own borders
improvement.hasUnique(UniqueType.CanBuildJustOutsideBorders, stateForConditionals)
&& neighbors.any { it.getOwner() == civInfo }
)
) -> false
improvement.getMatchingUniques(UniqueType.OnlyAvailableWhen, StateForConditionals.IgnoreConditionals).any {
!it.conditionalsApply(stateForConditionals)
} -> false
improvement.getMatchingUniques(UniqueType.ObsoleteWith, stateForConditionals).any {
civInfo.tech.isResearched(it.params[0])
} -> false
improvement.getMatchingUniques(UniqueType.ConsumesResources, stateForConditionals).any {
civInfo.getCivResourcesByName()[it.params[1]]!! < it.params[0].toInt()
} -> false
improvement.hasUnique(UniqueType.Unbuildable, stateForConditionals) -> false
else -> canImprovementBeBuiltHere(improvement, hasViewableResource(civInfo), stateForConditionals)
}
}
/** Without regards to what CivInfo it is, a lot of the checks are just for the improvement on the tile.
* Doubles as a check for the map editor.
*/
fun canImprovementBeBuiltHere(improvement: TileImprovement, resourceIsVisible: Boolean = resource != null): Boolean {
private fun canImprovementBeBuiltHere(
improvement: TileImprovement,
resourceIsVisible: Boolean = resource != null,
stateForConditionals: StateForConditionals = StateForConditionals(tile=this)
): Boolean {
val topTerrain = getLastTerrain()
return when {
improvement.name == this.improvement -> false
isCityCenter() -> false
improvement.getMatchingUniques(UniqueType.CannotBuildOnTile, StateForConditionals(tile = this)).any {
// First we handle a few special improvements
// Can only cancel if there is actually an improvement being built
improvement.name == Constants.cancelImprovementOrder -> (this.improvementInProgress != null)
// Can only remove if the feature is actually there
improvement.name.startsWith(Constants.remove) -> terrainFeatures.any { it == improvement.name.removePrefix(Constants.remove) }
// Can only build roads if on land and they are better than the current road
RoadStatus.values().any { it.name == improvement.name } -> !isWater && RoadStatus.valueOf(improvement.name) > roadStatus
// Can always build road removal improvements
improvement.name == roadStatus.removeAction -> true
// Then we check if there is any reason to not allow this improvement to be build
// Can't build if there is already an irremovable improvement here
this.improvement != null && getTileImprovement()!!.hasUnique(UniqueType.Irremovable, stateForConditionals) -> false
// Can't build if any terrain specifically prevents building this improvement
getTerrainMatchingUniques(UniqueType.RestrictedBuildableImprovements, stateForConditionals).any {
unique -> !improvement.matchesFilter(unique.params[0])
} -> false
// Can't build if the improvement specifically prevents building on some present feature
improvement.getMatchingUniques(UniqueType.CannotBuildOnTile, stateForConditionals).any {
unique -> matchesTerrainFilter(unique.params[0])
} -> false
// Road improvements can change on tiles with irremovable improvements - nothing else can, though.
RoadStatus.values().none { it.name == improvement.name || it.removeAction == improvement.name }
&& getTileImprovement().let { it != null && it.hasUnique( UniqueType.Irremovable) } -> false
// Terrain blocks BUILDING improvements - removing things (such as fallout) is fine
!improvement.name.startsWith(Constants.remove) &&
getAllTerrains().any { it.getMatchingUniques(UniqueType.RestrictedBuildableImprovements)
.any { unique -> !improvement.matchesFilter(unique.params[0]) } } -> false
// Decide cancelImprovementOrder earlier, otherwise next check breaks it
improvement.name == Constants.cancelImprovementOrder -> (this.improvementInProgress != null)
// Tiles with no terrains, and no turns to build, are like great improvements - they're placeable
improvement.terrainsCanBeBuiltOn.isEmpty() && improvement.turnsToBuild == 0 && isLand -> true
improvement.terrainsCanBeBuiltOn.contains(topTerrain.name) -> true
improvement.getMatchingUniques(UniqueType.MustBeNextTo).any {
// Can't build if an improvement is only allowed to be built on specific tiles and this is not one of them
// If multiple uniques of this type exists, we want all to match (e.g. Hill _and_ Forest would be meaningful)
improvement.getMatchingUniques(UniqueType.CanOnlyBeBuiltOnTile, stateForConditionals).let {
it.any() && it.any { unique -> !matchesTerrainFilter(unique.params[0]) }
} -> false
// Can't build if the improvement requires an adjacent terrain that is not present
improvement.getMatchingUniques(UniqueType.MustBeNextTo, stateForConditionals).any {
!isAdjacentTo(it.params[0])
} -> false
!isWater && RoadStatus.values().any { it.name == improvement.name && it > roadStatus } -> true
improvement.name == roadStatus.removeAction -> true
// Can't build on unbuildable terrains - EXCEPT when specifically allowed to
topTerrain.unbuildable && !improvement.isAllowedOnFeature(topTerrain.name) -> false
// Can't build it if it is only allowed to improve resources and it doesn't improve this reousrce
improvement.hasUnique(UniqueType.CanOnlyImproveResource, stateForConditionals) && (
!resourceIsVisible || !tileResource.isImprovedBy(improvement.name)
) -> false
// 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
improvement.canBeBuiltOn(topTerrain.name) -> true
isLand && improvement.canBeBuiltOn("Land") -> true
isWater && improvement.canBeBuiltOn("Water") -> true
// DO NOT reverse this &&. isAdjacentToFreshwater() is a lazy which calls a function, and reversing it breaks the tests.
improvement.hasUnique(UniqueType.ImprovementBuildableByFreshWater) && isAdjacentTo(Constants.freshWater) -> true
// If an unique of this type exists, we want all to match (e.g. Hill _and_ Forest would be meaningful).
improvement.getMatchingUniques(UniqueType.CanOnlyBeBuiltOnTile).let {
it.any() && it.all { unique -> matchesTerrainFilter(unique.params[0]) }
} -> true
else -> resourceIsVisible && tileResource.improvement == improvement.name
improvement.hasUnique(UniqueType.ImprovementBuildableByFreshWater, stateForConditionals)
&& isAdjacentTo(Constants.freshWater) -> true
// I don't particularly like this check, but it is required to build mines on non-hill resources
resourceIsVisible && tileResource.isImprovedBy(improvement.name) -> true
// DEPRECATED since 4.0.14, REMOVE SOON:
isLand && improvement.terrainsCanBeBuiltOn.isEmpty() && !improvement.hasUnique(UniqueType.CanOnlyImproveResource) -> true
else -> false
}
}
@ -739,7 +769,8 @@ open class TileInfo {
else
FormattedLine(resource!!, link="Resource/$resource")
if (resource != null && viewingCiv != null && hasViewableResource(viewingCiv)) {
val tileImprovement = ruleset.tileImprovements[tileResource.improvement]
val resourceImprovement = tileResource.getImprovements().firstOrNull { canBuildImprovement(ruleset.tileImprovements[it]!!, viewingCiv) }
val tileImprovement = ruleset.tileImprovements[resourceImprovement]
if (tileImprovement?.techRequired != null
&& !viewingCiv.tech.isResearched(tileImprovement.techRequired!!)) {
lineList += FormattedLine(
@ -1005,11 +1036,7 @@ open class TileInfo {
return
}
improvement = null // Unset, and check if it can be reset. If so, do it, if not, invalid.
if (canImprovementBeBuiltHere(improvementObject)
// Allow building 'other' improvements like city ruins, barb encampments, Great Improvements etc
|| (improvementObject.terrainsCanBeBuiltOn.isEmpty()
&& ruleset.tileResources.values.none { it.improvement == improvementObject.name }
&& !isImpassible() && isLand))
if (canImprovementBeBuiltHere(improvementObject, stateForConditionals = StateForConditionals.IgnoreConditionals))
improvement = improvementObject.name
}

View File

@ -360,7 +360,7 @@ class TileMap {
|| cTileHeight > bNeighborHeight // c>b
|| (
currentTileHeight == bNeighborHeight // a==b
&& !bNeighbor.hasUnique(UniqueType.BlocksLineOfSightAtSameElevation)
&& !bNeighbor.terrainHasUnique(UniqueType.BlocksLineOfSightAtSameElevation)
)
)
}

View File

@ -660,7 +660,7 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
it.resource != null
&& requiredNearbyImprovedResources!!.contains(it.resource!!)
&& it.getOwner() == civInfo
&& (it.tileResource.improvement == it.improvement || it.isCityCenter()
&& ((it.improvement != null && it.tileResource.isImprovedBy(it.improvement!!)) || it.isCityCenter()
|| (it.getTileImprovement()?.isGreatImprovement() == true && it.tileResource.resourceType == ResourceType.Strategic)
)
}

View File

@ -9,6 +9,7 @@ import com.unciv.json.json
import com.unciv.logic.BackwardCompatibility.updateDeprecations
import com.unciv.logic.UncivShowableException
import com.unciv.logic.map.MapParameters
import com.unciv.logic.map.RoadStatus
import com.unciv.models.Counter
import com.unciv.models.ModConstants
import com.unciv.models.metadata.BaseRuleset
@ -665,6 +666,9 @@ class Ruleset {
lines += "${resource.name} revealed by tech ${resource.revealedBy} which does not exist!"
if (resource.improvement != null && !tileImprovements.containsKey(resource.improvement!!))
lines += "${resource.name} improved by improvement ${resource.improvement} which does not exist!"
for (improvement in resource.improvedBy)
if (!tileImprovements.containsKey(improvement))
lines += "${resource.name} improved by improvement $improvement which does not exist!"
for (terrain in resource.terrainsCanBeFoundOn)
if (!terrains.containsKey(terrain))
lines += "${resource.name} can be found on terrain $terrain which does not exist!"
@ -675,8 +679,20 @@ class Ruleset {
if (improvement.techRequired != null && !technologies.containsKey(improvement.techRequired!!))
lines += "${improvement.name} requires tech ${improvement.techRequired} which does not exist!"
for (terrain in improvement.terrainsCanBeBuiltOn)
if (!terrains.containsKey(terrain))
if (!terrains.containsKey(terrain) && terrain != "Land" && terrain != "Water")
lines += "${improvement.name} can be built on terrain $terrain which does not exist!"
if (improvement.terrainsCanBeBuiltOn.isEmpty()
&& !improvement.hasUnique(UniqueType.CanOnlyImproveResource)
&& !improvement.hasUnique(UniqueType.Unbuildable)
&& !improvement.name.startsWith(Constants.remove)
&& improvement.name !in RoadStatus.values().map { it.removeAction }
&& improvement.name != Constants.cancelImprovementOrder
) {
lines.add(
"${improvement.name} has an empty `terrainsCanBeBuiltOn`, isn't allowed to only improve resources and isn't unbuildable! Support for this will soon end. Either give this the unique \"Unbuildable\", \"Can only be built to improve a resource\" or add \"Land\", \"Water\" or any other value to `terrainsCanBeBuiltOn`.",
RulesetErrorSeverity.Warning
)
}
checkUniques(improvement, lines, rulesetSpecific, forOptionsPopup)
}

View File

@ -13,6 +13,7 @@ import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.tr
import com.unciv.ui.civilopedia.FormattedLine
import com.unciv.ui.utils.toPercent
import com.unciv.ui.worldscreen.unit.UnitActions
import java.util.*
import kotlin.math.roundToInt
@ -23,16 +24,17 @@ class TileImprovement : RulesetStatsObject() {
var uniqueTo:String? = null
override fun getUniqueTarget() = UniqueTarget.Improvement
val shortcutKey: Char? = null
val turnsToBuild: Int = 0 // This is the base cost.
// This is the base cost. A cost of 0 means created instead of buildable.
val turnsToBuild: Int = 0
fun getTurnsToBuild(civInfo: CivilizationInfo, unit: MapUnit?): Int {
fun getTurnsToBuild(civInfo: CivilizationInfo, unit: MapUnit): Int {
val state = StateForConditionals(civInfo, unit = unit)
val uniques = civInfo.getMatchingUniques(UniqueType.TileImprovementTime, state) +
(unit?.getMatchingUniques(UniqueType.TileImprovementTime, state) ?: sequenceOf())
return uniques.fold(turnsToBuild.toFloat() * civInfo.gameInfo.gameParameters.gameSpeed.modifier) {
it, unique -> it * unique.params[0].toPercent()
}.roundToInt().coerceAtLeast(1)
return unit.getMatchingUniques(UniqueType.TileImprovementTime, state, checkCivInfoUniques = true)
.fold(turnsToBuild.toFloat() * civInfo.gameInfo.gameParameters.gameSpeed.modifier) { it, unique ->
it * unique.params[0].toPercent()
}.roundToInt()
.coerceAtLeast(1)
// In some weird cases it was possible for something to take 0 turns, leading to it instead never finishing
}
@ -48,7 +50,7 @@ class TileImprovement : RulesetStatsObject() {
}
lines += "Can be built on".tr() + terrainsCanBeBuiltOnString.joinToString(", ", " ") //language can be changed when setting changes.
}
for (resource: TileResource in ruleset.tileResources.values.filter { it.improvement == name }) {
for (resource: TileResource in ruleset.tileResources.values.filter { it.isImprovedBy(name) }) {
if (resource.improvementStats == null) continue
val statsString = resource.improvementStats.toString()
lines += "[${statsString}] <in [${resource.name}] tiles>".tr()
@ -65,6 +67,22 @@ class TileImprovement : RulesetStatsObject() {
fun isRoad() = RoadStatus.values().any { it != RoadStatus.None && it.name == this.name }
fun isAncientRuinsEquivalent() = hasUnique(UniqueType.IsAncientRuinsEquivalent)
fun canBeBuiltOn(terrain: String): Boolean {
return terrain in terrainsCanBeBuiltOn
}
fun handleImprovementCompletion(builder: MapUnit) {
if (hasUnique(UniqueType.TakesOverAdjacentTiles))
UnitActions.takeOverTilesAround(builder)
if (builder.getTile().resource != null) {
val city = builder.getTile().getCity()
if (city != null) {
city.cityStats.update()
city.civInfo.updateDetailedCivResources()
}
}
}
/**
* Check: Is this improvement allowed on a [given][name] terrain feature?
*
@ -75,7 +93,7 @@ class TileImprovement : RulesetStatsObject() {
* 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.
*/
fun isAllowedOnFeature(name: String) = getMatchingUniques(UniqueType.NoFeatureRemovalNeeded).any { it.params[0] == name }
fun isAllowedOnFeature(name: String) = terrainsCanBeBuiltOn.contains(name) || getMatchingUniques(UniqueType.NoFeatureRemovalNeeded).any { it.params[0] == name }
/** Implements [UniqueParameterType.ImprovementFilter][com.unciv.models.ruleset.unique.UniqueParameterType.ImprovementFilter] */
fun matchesFilter(filter: String): Boolean {
@ -117,7 +135,7 @@ class TileImprovement : RulesetStatsObject() {
}
var addedLineBeforeResourceBonus = false
for (resource in ruleset.tileResources.values.filter { it.improvement == name }) {
for (resource in ruleset.tileResources.values.filter { it.isImprovedBy(name) }) {
if (resource.improvementStats == null) continue
if (!addedLineBeforeResourceBonus) {
addedLineBeforeResourceBonus = true

View File

@ -1,5 +1,7 @@
package com.unciv.models.ruleset.tile
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.TileInfo
import com.unciv.models.ruleset.Belief
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetStatsObject
@ -15,12 +17,21 @@ class TileResource : RulesetStatsObject() {
var improvement: String? = null
var improvementStats: Stats? = null
var revealedBy: String? = null
var improvedBy: List<String> = listOf()
var majorDepositAmount: DepositAmount = DepositAmount()
var minorDepositAmount: DepositAmount = DepositAmount()
val _allImprovements by lazy {
if (improvement == null) improvedBy
else improvedBy + improvement!!
}
fun getImprovements(): List<String> {
return _allImprovements
}
override fun getUniqueTarget() = UniqueTarget.Resource
override fun makeLink() = "Resource/$name"
override fun getCivilopediaTextLines(ruleset: Ruleset): List<FormattedLine> {
@ -45,7 +56,7 @@ class TileResource : RulesetStatsObject() {
}
}
if (improvement != null) {
for (improvement in getImprovements()) {
textList += FormattedLine()
textList += FormattedLine("Improved by [$improvement]", link = "Improvement/$improvement")
if (improvementStats != null && !improvementStats!!.isEmpty())
@ -114,6 +125,16 @@ class TileResource : RulesetStatsObject() {
return textList
}
fun isImprovedBy(improvementName: String): Boolean {
return getImprovements().contains(improvementName)
}
fun getImprovingImprovement(tile: TileInfo, civInfo: CivilizationInfo): String? {
return getImprovements().firstOrNull {
tile.canBuildImprovement(civInfo.gameInfo.ruleSet.tileImprovements[it]!!, civInfo)
}
}
class DepositAmount {
var sparse: Int = 1
var default: Int = 2

View File

@ -316,7 +316,7 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
///////////////////////////////////////// region CONSTRUCTION UNIQUES /////////////////////////////////////////
Unbuildable("Unbuildable", UniqueTarget.Building, UniqueTarget.Unit),
Unbuildable("Unbuildable", UniqueTarget.Building, UniqueTarget.Unit, UniqueTarget.Improvement),
CannotBePurchased("Cannot be purchased", UniqueTarget.Building, UniqueTarget.Unit),
CanBePurchasedWithStat("Can be purchased with [stat] [cityFilter]", UniqueTarget.Building, UniqueTarget.Unit),
CanBePurchasedForAmountStat("Can be purchased for [amount] [stat] [cityFilter]", UniqueTarget.Building, UniqueTarget.Unit),
@ -437,6 +437,7 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
NoDefensiveTerrainPenalty("No defensive terrain penalty", UniqueTarget.Unit, UniqueTarget.Global),
NoDamagePenalty("Damage is ignored when determining unit Strength", UniqueTarget.Unit, UniqueTarget.Global),
Uncapturable("Uncapturable", UniqueTarget.Unit),
// Replace with "Withdraws before melee combat <with [amount]% chance>"?
MayWithdraw("May withdraw before melee ([amount]%)", UniqueTarget.Unit),
CannotCaptureCities("Unable to capture cities", UniqueTarget.Unit),
@ -614,6 +615,7 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
CanOnlyBeBuiltOnTile("Can only be built on [tileFilter] tiles", UniqueTarget.Improvement),
CannotBuildOnTile("Cannot be built on [tileFilter] tiles", UniqueTarget.Improvement),
NoFeatureRemovalNeeded("Does not need removal of [tileFilter]", UniqueTarget.Improvement),
CanOnlyImproveResource("Can only be built to improve a resource", UniqueTarget.Improvement),
DefensiveBonus("Gives a defensive bonus of [relativeAmount]%", UniqueTarget.Improvement),
ImprovementMaintenance("Costs [amount] gold per turn when in your territory", UniqueTarget.Improvement), // Unused
@ -622,9 +624,9 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
GreatImprovement("Great Improvement", UniqueTarget.Improvement),
IsAncientRuinsEquivalent("Provides a random bonus when entered", UniqueTarget.Improvement),
TakesOverAdjacentTiles("Constructing it will take over the tiles around it and assign them to your closest city", UniqueTarget.Improvement),
Unpillagable("Unpillagable", UniqueTarget.Improvement),
Indestructible("Indestructible", UniqueTarget.Improvement),
Irremovable("Irremovable", UniqueTarget.Improvement),
//endregion
@ -751,6 +753,9 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
// region DEPRECATED AND REMOVED
@Deprecated("as of 4.0.15", ReplaceWith("Irremovable"), DeprecationLevel.ERROR)
Indestructible("Indestructible", UniqueTarget.Improvement),
@Deprecated("as of 3.19.1", ReplaceWith("[stats] from every [Wonder]"), DeprecationLevel.ERROR)
StatsFromWondersDeprecated("[stats] from every Wonder", UniqueTarget.Global, UniqueTarget.FollowerBelief),
@Deprecated("as of 3.19.3", ReplaceWith("[stats] from every [buildingFilter] <in cities where this religion has at least [amount] followers>"), DeprecationLevel.ERROR)

View File

@ -229,7 +229,7 @@ class ResourcesOverviewTab(
if (!tile.hasViewableResource(viewingPlayer)) continue
val tileResource = tile.tileResource
if (tileResource.resourceType == ResourceType.Bonus) continue
if (tile.improvement == tileResource.improvement) continue
if (tile.improvement != null && tileResource.isImprovedBy(tile.improvement!!)) continue
if (tileResource.resourceType == ResourceType.Strategic && tile.getTileImprovement()?.isGreatImprovement() == true) continue
resourceSupplyList.add(tileResource, 1, ExtraInfoOrigin.Unimproved.name)
}

View File

@ -20,7 +20,7 @@ import kotlin.math.roundToInt
class ImprovementPickerScreen(
private val tileInfo: TileInfo,
private val unit: MapUnit,
private val onAccept: ()->Unit
private val onAccept: ()->Unit,
) : PickerScreen() {
private var selectedImprovement: TileImprovement? = null
private val gameInfo = tileInfo.tileMap.gameInfo
@ -98,12 +98,12 @@ class ImprovementPickerScreen(
var labelText = improvement.name.tr()
val turnsToBuild = if (tileInfo.improvementInProgress == improvement.name) tileInfo.turnsToImprovement
else improvement.getTurnsToBuild(currentPlayerCiv, unit)
else improvement.getTurnsToBuild(currentPlayerCiv, unit)
if (turnsToBuild > 0) labelText += " - $turnsToBuild${Fonts.turn}"
val provideResource = tileInfo.hasViewableResource(currentPlayerCiv) && tileInfo.tileResource.improvement == improvement.name
val provideResource = tileInfo.hasViewableResource(currentPlayerCiv) && tileInfo.tileResource.isImprovedBy(improvement.name)
if (provideResource) labelText += "\n" + "Provides [${tileInfo.resource}]".tr()
val removeImprovement = (improvement.name != RoadStatus.Road.name
&& improvement.name != RoadStatus.Railroad.name
val removeImprovement = (improvement.isRoad()
&& !improvement.name.startsWith(Constants.remove)
&& improvement.name != Constants.cancelImprovementOrder)
if (tileInfo.improvement != null && removeImprovement) labelText += "\n" + "Replaces [${tileInfo.improvement}]".tr()
@ -162,7 +162,7 @@ class ImprovementPickerScreen(
statIcons.add(ImageGetter.getResourceImage(tileInfo.resource.toString(), 30f)).pad(3f)
// icon for removing the resource by replacing improvement
if (removeImprovement && tileInfo.hasViewableResource(currentPlayerCiv) && tileInfo.tileResource.improvement == tileInfo.improvement) {
if (removeImprovement && tileInfo.hasViewableResource(currentPlayerCiv) && tileInfo.improvement != null && tileInfo.tileResource.isImprovedBy(tileInfo.improvement!!)) {
val resourceIcon = ImageGetter.getResourceImage(tileInfo.resource!!, 30f)
statIcons.add(ImageGetter.getCrossedImage(resourceIcon, 30f))
}

View File

@ -425,10 +425,8 @@ class DiplomacyScreen(
for (improvableTile in improvableResourceTiles)
for (tileImprovement in improvements.values)
if (improvableTile.canBuildImprovement(
tileImprovement,
otherCiv
) && improvableTile.tileResource.improvement == tileImprovement.name
if (improvableTile.tileResource.isImprovedBy(tileImprovement.name)
&& improvableTile.canBuildImprovement(tileImprovement, otherCiv)
)
needsImprovements = true
@ -490,9 +488,11 @@ class DiplomacyScreen(
return diplomacyTable
}
fun getImprovableResourceTiles(otherCiv:CivilizationInfo) = otherCiv.getCapital().getTiles()
.filter { it.hasViewableResource(otherCiv) && it.tileResource.resourceType!=ResourceType.Bonus
&& it.tileResource.improvement != it.improvement }
fun getImprovableResourceTiles(otherCiv:CivilizationInfo) = otherCiv.getCapital().getTiles().filter {
it.hasViewableResource(otherCiv)
&& it.tileResource.resourceType != ResourceType.Bonus
&& (it.improvement == null || !it.tileResource.isImprovedBy(it.improvement!!))
}
private fun getImprovementGiftTable(otherCiv: CivilizationInfo): Table {
val improvementGiftTable = getCityStateDiplomacyTableHeader(otherCiv)
@ -504,7 +504,7 @@ class DiplomacyScreen(
for (improvableTile in improvableResourceTiles) {
for (tileImprovement in tileImprovements.values) {
if (improvableTile.tileResource.improvement == tileImprovement.name
if (improvableTile.tileResource.isImprovedBy(tileImprovement.name)
&& improvableTile.canBuildImprovement(tileImprovement, otherCiv)
) {
val improveTileButton =

View File

@ -545,7 +545,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
displayTutorial(Tutorial.Workers) {
gameInfo.getCurrentPlayerCivilization().getCivUnits().any {
it.hasUniqueToBuildImprovements && it.isCivilian()
it.hasUniqueToBuildImprovements && it.isCivilian() && !it.isGreatPerson()
}
}
}

View File

@ -624,7 +624,9 @@ class OptionsPopup(
for ((tile, resource) in ownedTiles zip resourceTypes) {
tile.resource = resource.name
tile.resourceAmount = 999
tile.improvement = resource.improvement
// Debug option, so if it crashes on this that's relatively fine
// If this becomes a problem, check if such an improvement exists and otherwise plop down a great improvement or so
tile.improvement = resource.getImprovements().first()
}
game.gameInfo.getCurrentPlayerCivilization().updateSightAndResources()
game.worldScreen.shouldUpdate = true

View File

@ -19,7 +19,6 @@ import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.unique.UniqueTriggerActivation
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats
import com.unciv.models.translations.tr
import com.unciv.ui.pickerscreens.ImprovementPickerScreen
import com.unciv.ui.pickerscreens.PromotionPickerScreen
@ -135,7 +134,7 @@ object UnitActions {
val tile = unit.currentTile
if (!tile.isWater || !unit.hasUnique(UniqueType.CreateWaterImprovements) || tile.resource == null) return null
val improvementName = tile.tileResource.improvement ?: return null
val improvementName = tile.tileResource.getImprovingImprovement(tile, unit.civInfo) ?: return null
val improvement = tile.ruleset.tileImprovements[improvementName] ?: return null
if (!tile.canBuildImprovement(improvement, unit.civInfo)) return null
@ -439,7 +438,6 @@ object UnitActions {
actionList += getAddInCapitalAction(unit, tile)
}
private fun addGreatPersonActions(unit: MapUnit, actionList: ArrayList<UnitAction>, tile: TileInfo) {
if (unit.currentMovement > 0) for (unique in unit.getUniques()) when (unique.placeholderText) {
@ -447,8 +445,7 @@ object UnitActions {
actionList += UnitAction(UnitActionType.HurryResearch,
action = {
unit.civInfo.tech.addScience(unit.civInfo.tech.getScienceFromGreatScientist())
addStatsPerGreatPersonUsage(unit)
unit.destroy()
unit.consume()
}.takeIf { unit.civInfo.tech.currentTechnologyName() != null }
)
}
@ -457,8 +454,7 @@ object UnitActions {
actionList += UnitAction(UnitActionType.StartGoldenAge,
action = {
unit.civInfo.goldenAges.enterGoldenAge(turnsToGoldenAge)
addStatsPerGreatPersonUsage(unit)
unit.destroy()
unit.consume()
}.takeIf { unit.currentTile.getOwner() != null && unit.currentTile.getOwner() == unit.civInfo }
)
}
@ -476,8 +472,7 @@ object UnitActions {
constructIfEnough()
}
addStatsPerGreatPersonUsage(unit)
unit.destroy()
unit.consume()
}.takeIf { canHurryWonder }
)
}
@ -507,8 +502,7 @@ object UnitActions {
constructIfEnough()
}
addStatsPerGreatPersonUsage(unit)
unit.destroy()
unit.consume()
}.takeIf { canHurryConstruction }
)
}
@ -526,8 +520,7 @@ object UnitActions {
tile.owningCity!!.civInfo.getDiplomacyManager(unit.civInfo).addInfluence(influenceEarned)
unit.civInfo.addNotification("Your trade mission to [${tile.owningCity!!.civInfo}] has earned you [${goldEarned}] gold and [$influenceEarned] influence!",
tile.owningCity!!.civInfo.civName, NotificationIcon.Gold, NotificationIcon.Culture)
addStatsPerGreatPersonUsage(unit)
unit.destroy()
unit.consume()
}.takeIf { canConductTradeMission }
)
}
@ -544,9 +537,8 @@ object UnitActions {
fun getFoundReligionAction(unit: MapUnit): () -> Unit {
return {
addStatsPerGreatPersonUsage(unit)
unit.civInfo.religionManager.useProphetForFoundingReligion(unit)
unit.destroy()
unit.consume()
}
}
@ -561,9 +553,8 @@ object UnitActions {
fun getEnhanceReligionAction(unit: MapUnit): () -> Unit {
return {
addStatsPerGreatPersonUsage(unit)
unit.civInfo.religionManager.useProphetForEnhancingReligion(unit)
unit.destroy()
unit.consume()
}
}
@ -586,8 +577,7 @@ object UnitActions {
private fun useActionWithLimitedUses(unit: MapUnit, action: String) {
unit.abilityUsesLeft[action] = unit.abilityUsesLeft[action]!! - 1
if (unit.abilityUsesLeft[action]!! <= 0) {
addStatsPerGreatPersonUsage(unit)
unit.destroy()
unit.consume()
}
}
@ -665,16 +655,8 @@ object UnitActions {
unitTile.removeCreatesOneImprovementMarker()
unitTile.improvement = improvementName
unitTile.stopWorkingOnImprovement()
if (improvement.hasUnique(UniqueType.TakeOverTilesAroundWhenBuilt))
takeOverTilesAround(unit)
val city = unitTile.getCity()
if (city != null) {
city.cityStats.update()
city.civInfo.updateDetailedCivResources()
}
if (unit.isGreatPerson())
addStatsPerGreatPersonUsage(unit)
unit.destroy()
improvement.handleImprovementCompletion(unit)
unit.consume()
}.takeIf {
resourcesAvailable
&& unit.currentMovement > 0f
@ -688,7 +670,7 @@ object UnitActions {
return finalActions
}
private fun takeOverTilesAround(unit: MapUnit) {
fun takeOverTilesAround(unit: MapUnit) {
// This method should only be called for a citadel - therefore one of the neighbour tile
// must belong to unit's civ, so minByOrNull in the nearestCity formula should be never `null`.
// That is, unless a mod does not specify the proper unique - then fallbackNearestCity will take over.
@ -732,26 +714,6 @@ object UnitActions {
otherCiv.addNotification("[${unit.civInfo}] has stolen your territory!", unit.currentTile.position, unit.civInfo.civName, NotificationIcon.War)
}
private fun addStatsPerGreatPersonUsage(unit: MapUnit) {
if (!unit.isGreatPerson()) return
val civInfo = unit.civInfo
val gainedStats = Stats()
for (unique in civInfo.getMatchingUniques(UniqueType.ProvidesGoldWheneverGreatPersonExpended)) {
gainedStats.gold += (100 * civInfo.gameInfo.gameParameters.gameSpeed.modifier).toInt()
}
for (unique in civInfo.getMatchingUniques(UniqueType.ProvidesStatsWheneverGreatPersonExpended)) {
gainedStats.add(unique.stats)
}
if (gainedStats.isEmpty()) return
for (stat in gainedStats)
civInfo.addStat(stat.key, stat.value.toInt())
civInfo.addNotification("By expending your [${unit.name}] you gained [${gainedStats}]!", unit.getTile().position, unit.name)
}
private fun addFortifyActions(actionList: ArrayList<UnitAction>, unit: MapUnit, showingAdditionalActions: Boolean) {
if (unit.isFortified() && !showingAdditionalActions) {
actionList += UnitAction(
@ -860,8 +822,7 @@ object UnitActions {
if (!unique.conditionals.any { it.type == UniqueType.ConditionalConsumeUnit }) continue
val unitAction = UnitAction(type = UnitActionType.TriggerUnique, unique.text){
UniqueTriggerActivation.triggerCivwideUnique(unique, unit.civInfo)
addStatsPerGreatPersonUsage(unit)
unit.destroy()
unit.consume()
}
actionList += unitAction
}

View File

@ -212,6 +212,7 @@ Unless otherwise specified, all the following are from [the Noun Project](https:
- [Citadel](https://thenounproject.com/term/fort/1697646/) By Adrien Coquet
- [Village](https://thenounproject.com/search/?q=city+center&i=476338) by Andrey Vasiliev
- [pumping station](https://thenounproject.com/search/?q=dike&i=555172) by Peter van Driel for Polder
- [Oil Platform](https://thenounproject.com/icon/oil-platform-1364795/) by Georgiana Ionescu for Offshore Platform
### Buildings

View File

@ -829,7 +829,7 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
Applicable to: Building, Improvement
??? example "Unbuildable"
Applicable to: Building, Unit
Applicable to: Building, Unit, Improvement
??? example "Cannot be purchased"
Applicable to: Building, Unit
@ -1396,6 +1396,9 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
Applicable to: Improvement
??? example "Can only be built to improve a resource"
Applicable to: Improvement
??? example "Gives a defensive bonus of [relativeAmount]%"
Example: "Gives a defensive bonus of [+20]%"
@ -1420,10 +1423,10 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
??? example "Provides a random bonus when entered"
Applicable to: Improvement
??? example "Unpillagable"
??? example "Constructing it will take over the tiles around it and assign them to your closest city"
Applicable to: Improvement
??? example "Indestructible"
??? example "Unpillagable"
Applicable to: Improvement
??? example "Irremovable"

View File

@ -6,6 +6,9 @@ import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.tile.TerrainType
import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.testing.GdxTestRunner
import org.junit.Assert
import org.junit.Before
@ -42,11 +45,23 @@ class TileImprovementConstructionTests {
fun allTerrainSpecificImprovementsCanBeBuilt() {
for (improvement in ruleSet.tileImprovements.values) {
val terrain = improvement.terrainsCanBeBuiltOn.firstOrNull() ?: continue
var terrain = improvement.terrainsCanBeBuiltOn.firstOrNull() ?: continue
if (terrain == "Land") terrain = ruleSet.terrains.values.first { it.type == TerrainType.Land }.name
if (terrain == "Water") terrain = ruleSet.terrains.values.first { it.type == TerrainType.Water }.name
// If this improvement requires additional conditions to be true,
// its too complex to handle all of them, so just skip it and hope its fine
// I would like some comments on whether this approach is fine or if it's better if I handle every single unique here as well
if (improvement.hasUnique(UniqueType.CanOnlyBeBuiltOnTile, StateForConditionals.IgnoreConditionals)) continue
if (improvement.hasUnique(UniqueType.Unbuildable, StateForConditionals.IgnoreConditionals)) continue
val tile = getTile()
tile.baseTerrain = terrain
if (improvement.hasUnique(UniqueType.CanOnlyImproveResource, StateForConditionals.IgnoreConditionals)) {
tile.resource = ruleSet.tileResources.values.firstOrNull { it.isImprovedBy(improvement.name) }?.name ?: continue
}
tile.setTransients()
if (improvement.uniqueTo != null) civInfo.civName = improvement.uniqueTo!!
val canBeBuilt = tile.canBuildImprovement(improvement, civInfo)
Assert.assertTrue(improvement.name, canBeBuilt)
}
@ -57,8 +72,12 @@ class TileImprovementConstructionTests {
for (improvement in ruleSet.tileImprovements.values) {
val tile = getTile()
tile.resource = ruleSet.tileResources.values
.firstOrNull { it.improvement == improvement.name }?.name
.firstOrNull { it.isImprovedBy(improvement.name) }?.name
if (tile.resource == null) continue
// If this improvement requires additional conditions to be true,
// its too complex to handle all of them, so just skip it and hope its fine
if (improvement.hasUnique(UniqueType.CanOnlyBeBuiltOnTile, StateForConditionals.IgnoreConditionals)) continue
tile.setTransients()
val canBeBuilt = tile.canBuildImprovement(improvement, civInfo)
Assert.assertTrue(improvement.name, canBeBuilt)
@ -116,11 +135,11 @@ class TileImprovementConstructionTests {
civInfo.civName = "OtherCiv"
for (resource in ruleSet.tileResources.values) {
if (resource.improvement == null) continue
val improvement = ruleSet.tileImprovements[resource.improvement]!!
if (improvement.terrainsCanBeBuiltOn.isNotEmpty()) continue
if (resource.getImprovements().isEmpty()) continue
val improvement = ruleSet.tileImprovements[resource.getImprovements().first()]!!
if (!improvement.hasUnique(UniqueType.CanOnlyImproveResource)) continue
val wrongResource = ruleSet.tileResources.values.firstOrNull {
it != resource && it.improvement != improvement.name
it != resource && !it.isImprovedBy(improvement.name)
} ?: continue
val tile = getTile()
tile.baseTerrain = "Plains"