diff --git a/android/assets/jsons/Civ V - Gods & Kings/Terrains.json b/android/assets/jsons/Civ V - Gods & Kings/Terrains.json index 410ef250fc..893a54cc20 100644 --- a/android/assets/jsons/Civ V - Gods & Kings/Terrains.json +++ b/android/assets/jsons/Civ V - Gods & Kings/Terrains.json @@ -122,7 +122,7 @@ "defenceBonus": 0.25, "RGB": [120, 120, 120], "uniques": ["Rough terrain", - "Has an elevation of [4] for visibility calculations", + "Has an elevation of [2] for visibility calculations", "Occurs in chains at high elevations", "Units ending their turn on this terrain take [50] damage", "Always Fertility [-2] for Map Generation", @@ -158,7 +158,7 @@ "occursOn": ["Tundra","Plains","Grassland","Desert","Snow"], "uniques": ["Rough terrain", "[+5] Strength for cities built on this terrain", - "Has an elevation of [2] for visibility calculations", + "Has an elevation of [1] for visibility calculations", "Occurs in groups around high elevations", "[+1] to Fertility for Map Generation", "A Region is formed with at least [40]% [Hill] tiles, with priority [5]", diff --git a/android/assets/jsons/Civ V - Vanilla/Terrains.json b/android/assets/jsons/Civ V - Vanilla/Terrains.json index 1a01903214..de53c4eed4 100644 --- a/android/assets/jsons/Civ V - Vanilla/Terrains.json +++ b/android/assets/jsons/Civ V - Vanilla/Terrains.json @@ -122,7 +122,7 @@ "defenceBonus": 0.25, "RGB": [120, 120, 120], "uniques": ["Rough terrain", - "Has an elevation of [4] for visibility calculations", + "Has an elevation of [2] for visibility calculations", "Occurs in chains at high elevations", "Units ending their turn on this terrain take [50] damage", "Always Fertility [-2] for Map Generation", @@ -157,7 +157,7 @@ "occursOn": ["Tundra","Plains","Grassland","Desert","Snow"], "uniques": ["Rough terrain", "[+5] Strength for cities built on this terrain", - "Has an elevation of [2] for visibility calculations", + "Has an elevation of [1] for visibility calculations", "Occurs in groups around high elevations", "[+1] to Fertility for Map Generation", "A Region is formed with at least [40]% [Hill] tiles, with priority [5]", diff --git a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt index 784b0fe4df..0337f45541 100644 --- a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt @@ -40,7 +40,7 @@ object UnitAutomation { unit.movement.getDistanceToTiles().keys.filter { isGoodTileToExplore(unit, it) } if (explorableTilesThisTurn.any()) { val bestTile = explorableTilesThisTurn - .sortedByDescending { it.height } // secondary sort is by 'how far can you see' + .sortedByDescending { it.tileHeight } // secondary sort is by 'how far can you see' .maxByOrNull { it.aerialDistanceTo(unit.currentTile) }!! // primary sort is by 'how far can you go' unit.movement.headTowards(bestTile) return true diff --git a/core/src/com/unciv/logic/map/TileInfo.kt b/core/src/com/unciv/logic/map/TileInfo.kt index ad533135fa..714e382d6d 100644 --- a/core/src/com/unciv/logic/map/TileInfo.kt +++ b/core/src/com/unciv/logic/map/TileInfo.kt @@ -345,7 +345,13 @@ open class TileInfo : IsPartOfGameInfoSerialization { } @delegate:Transient - val height : Int by lazy { + val tileHeight : Int by lazy { // for e.g. hill+forest this is 2, since forest is visible above units + if (terrainHasUnique(UniqueType.BlocksLineOfSightAtSameElevation)) unitHeight + 1 + else unitHeight + } + + @delegate:Transient + val unitHeight : Int by lazy { // for e.g. hill+forest this is 1, since only hill provides height for units allTerrains.flatMap { it.getMatchingUniques(UniqueType.VisibilityElevation) } .map { it.params[0].toInt() }.sum() } diff --git a/core/src/com/unciv/logic/map/TileMap.kt b/core/src/com/unciv/logic/map/TileMap.kt index 8fafe23975..49dc16aff4 100644 --- a/core/src/com/unciv/logic/map/TileMap.kt +++ b/core/src/com/unciv/logic/map/TileMap.kt @@ -12,6 +12,7 @@ import com.unciv.models.ruleset.Nation import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.tile.TerrainType import com.unciv.models.ruleset.unique.UniqueType +import java.lang.Integer.max import kotlin.math.abs /** An Unciv map with all properties as produced by the [map editor][com.unciv.ui.mapeditor.MapEditorScreen] @@ -325,52 +326,50 @@ class TileMap : IsPartOfGameInfoSerialization { vectorUnwrappedLeft } + data class ViewableTile(val tile:TileInfo, val maxHeightSeenToTile:Int, val isVisible:Boolean, val isAttackable: Boolean) + /** @return List of tiles visible from location [position] for a unit with sight range [sightDistance] */ fun getViewableTiles(position: Vector2, sightDistance: Int): List { - val viewableTiles = getTilesInDistance(position, 1).toMutableList() - val currentTileHeight = get(position).height + val aUnitHeight = get(position).unitHeight + val viewableTiles = mutableListOf(ViewableTile( + get(position), + aUnitHeight, + isVisible = true, + isAttackable = false + )) for (i in 1..sightDistance) { // in each layer, // This is so we don't use tiles in the same distance to "see over", // that is to say, the "viewableTiles.contains(it) check will return false for neighbors from the same distance - val tilesToAddInDistanceI = ArrayList() + val tilesToAddInDistanceI = ArrayList() for (cTile in getTilesAtDistance(position, i)) { // for each tile in that layer, - val cTileHeight = cTile.height + val cTileHeight = cTile.tileHeight /* - Okay so, if we're looking at a tile from a to c with b in the middle, + Okay so, if we're looking at a tile from height a to one with height c with a MAXIMUM HEIGHT of b in the middle, we have several scenarios: - 1. a>b - - I can see everything, b does not hide c - 2. a==b - 2.1 c>b - c is tall enough I can see it over b! - 2.2 b blocks view from same-elevation tiles - hides c - 2.3 none of the above - I can see c + 1. a>=b - I can see everything, b does not hide c (equals is 'flat plain' or 'string of hills' or 'hill viewing over forests') 3. a=c - b hides c - 3.2 b=c - b hides c (hills hide other hills, forests, etc) + 3.2 bb || c>b || (a==b && b !blocks same-elevation view)" + This can all be summed up as "I can see c if a=>b || c>b" */ + val bMinimumHighestSeenTerrainSoFar = viewableTiles.filter { it.tile in cTile.neighbors } + .minOf { it.maxHeightSeenToTile } - val containsViewableNeighborThatCanSeeOver = cTile.neighbors.any { bNeighbor: TileInfo -> - val bNeighborHeight = bNeighbor.height - viewableTiles.contains(bNeighbor) - && ( - currentTileHeight > bNeighborHeight // a>b - || cTileHeight > bNeighborHeight // c>b - || ( - currentTileHeight == bNeighborHeight // a==b - && !bNeighbor.terrainHasUnique(UniqueType.BlocksLineOfSightAtSameElevation) - ) - ) - } - if (containsViewableNeighborThatCanSeeOver) tilesToAddInDistanceI.add(cTile) + tilesToAddInDistanceI.add(ViewableTile( + cTile, + max(cTileHeight, bMinimumHighestSeenTerrainSoFar), + aUnitHeight >= bMinimumHighestSeenTerrainSoFar || cTileHeight > bMinimumHighestSeenTerrainSoFar, + aUnitHeight >= bMinimumHighestSeenTerrainSoFar || cTile.unitHeight > bMinimumHighestSeenTerrainSoFar, + )) } viewableTiles.addAll(tilesToAddInDistanceI) } - return viewableTiles + return viewableTiles.filter { it.isVisible }.map { it.tile } } /** Strips all units from [TileMap] diff --git a/tests/src/com/unciv/logic/map/VisibilityTests.kt b/tests/src/com/unciv/logic/map/VisibilityTests.kt index 65b6e306d3..f7a01bd4b0 100644 --- a/tests/src/com/unciv/logic/map/VisibilityTests.kt +++ b/tests/src/com/unciv/logic/map/VisibilityTests.kt @@ -67,7 +67,7 @@ class VisibilityTests { fun canSeeForestOverPlains() { val grassland = addTile("Grassland", Vector2(0f,0f)) addTile("Plains", Vector2(1f,0f)) - val forest = addTile(listOf("Grassland", "Forest"), Vector2(2f, 1f)) + val forest = addTile(listOf("Grassland", "Forest"), Vector2(2f, 0f)) val viewableTiles = grassland.getViewableTilesList(2) assert(viewableTiles.contains(forest)) } @@ -75,7 +75,7 @@ class VisibilityTests { @Test fun cannotSeePlainsOverForest() { val grassland = addTile("Grassland", Vector2(0f,0f)) - addTile(listOf("Grassland", "Forest"), Vector2(1f, 1f)) + addTile(listOf("Grassland", "Forest"), Vector2(1f, 0f)) val plains = addTile("Plains", Vector2(2f,0f)) val viewableTiles = grassland.getViewableTilesList(2) assert(!viewableTiles.contains(plains)) @@ -84,7 +84,7 @@ class VisibilityTests { @Test fun cannotSeeForestOverForest() { val grassland = addTile("Grassland", Vector2(0f,0f)) - addTile(listOf("Grassland", "Forest"), Vector2(1f, 1f)) + addTile(listOf("Grassland", "Forest"), Vector2(1f, 0f)) val plains = addTile(listOf("Plains", "Forest"), Vector2(2f,0f)) val viewableTiles = grassland.getViewableTilesList(2) assert(!viewableTiles.contains(plains)) @@ -94,7 +94,7 @@ class VisibilityTests { fun canSeeHillOverPlains() { val grassland = addTile("Grassland", Vector2(0f,0f)) addTile("Plains", Vector2(1f,0f)) - val hill = addTile(listOf("Grassland", "Hill"), Vector2(2f, 1f)) + val hill = addTile(listOf("Grassland", "Hill"), Vector2(2f, 0f)) val viewableTiles = grassland.getViewableTilesList(2) assert(viewableTiles.contains(hill)) } @@ -102,10 +102,65 @@ class VisibilityTests { @Test fun cannotSeePlainsOverHill() { val grassland = addTile("Grassland", Vector2(0f,0f)) - addTile(listOf("Grassland", "Hill"), Vector2(1f, 1f)) + addTile(listOf("Grassland", "Hill"), Vector2(1f, 0f)) val plains = addTile("Plains", Vector2(2f,0f)) val viewableTiles = grassland.getViewableTilesList(2) assert(!viewableTiles.contains(plains)) } + @Test + fun cannotSeeHillOverHill() { + val grassland = addTile("Grassland", Vector2(0f,0f)) + addTile(listOf("Grassland", "Hill"), Vector2(1f,0f)) + val hill = addTile(listOf("Grassland", "Hill"), Vector2(2f, 0f)) + val viewableTiles = grassland.getViewableTilesList(2) + assert(!viewableTiles.contains(hill)) + } + + + @Test + fun cannotSeeHillOverForest() { + val grassland = addTile("Grassland", Vector2(0f,0f)) + addTile(listOf("Grassland", "Forest"), Vector2(1f,0f)) + val hill = addTile(listOf("Grassland", "Hill"), Vector2(2f, 0f)) + val viewableTiles = grassland.getViewableTilesList(2) + assert(!viewableTiles.contains(hill)) + } + + @Test + fun cannotSeeForestOverHill() { + val grassland = addTile("Grassland", Vector2(0f,0f)) + addTile(listOf("Grassland", "Hill"), Vector2(1f,0f)) + val hill = addTile(listOf("Grassland", "Forest"), Vector2(2f, 0f)) + val viewableTiles = grassland.getViewableTilesList(2) + assert(!viewableTiles.contains(hill)) + } + + @Test + fun canSeeHillForestOverHill() { + val grassland = addTile("Grassland", Vector2(0f,0f)) + addTile(listOf("Grassland", "Forest"), Vector2(1f,0f)) + val hill = addTile(listOf("Grassland", "Hill", "Forest"), Vector2(2f, 0f)) + val viewableTiles = grassland.getViewableTilesList(2) + assert(viewableTiles.contains(hill)) + } + + @Test + fun canSeeMountainOverHill() { + val grassland = addTile("Grassland", Vector2(0f,0f)) + addTile(listOf("Grassland", "Hill"), Vector2(1f,0f)) + val hill = addTile(listOf("Mountain"), Vector2(2f, 0f)) + val viewableTiles = grassland.getViewableTilesList(2) + assert(viewableTiles.contains(hill)) + } + + @Test + fun cannotSeeMountainOverHillForest() { + val grassland = addTile("Grassland", Vector2(0f,0f)) + addTile(listOf("Grassland", "Hill", "Forest"), Vector2(1f,0f)) + val hill = addTile(listOf("Mountain"), Vector2(2f, 0f)) + val viewableTiles = grassland.getViewableTilesList(2) + assert(!viewableTiles.contains(hill)) + } + }