diff --git a/core/src/com/unciv/UncivGame.kt b/core/src/com/unciv/UncivGame.kt index 161a5f2b2e..8cd83f04f8 100644 --- a/core/src/com/unciv/UncivGame.kt +++ b/core/src/com/unciv/UncivGame.kt @@ -252,7 +252,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { * * Sets the returned `WorldScreen` as the only active screen. */ - suspend fun loadGame(newGameInfo: GameInfo): WorldScreen = withThreadPoolContext toplevel@{ + suspend fun loadGame(newGameInfo: GameInfo, callFromLoadScreen: Boolean = false): WorldScreen = withThreadPoolContext toplevel@{ val prevGameInfo = gameInfo gameInfo = newGameInfo @@ -266,7 +266,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { initializeResources(prevGameInfo, newGameInfo) val isLoadingSameGame = worldScreen != null && prevGameInfo != null && prevGameInfo.gameId == newGameInfo.gameId - val worldScreenRestoreState = if (isLoadingSameGame) worldScreen!!.getRestoreState() else null + val worldScreenRestoreState = if (!callFromLoadScreen && isLoadingSameGame) worldScreen!!.getRestoreState() else null lateinit var loadingScreen: LoadingScreen diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index 1a2bf41a2c..6341e73f28 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -542,6 +542,8 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion }) { for (unit in civInfo.units.getCivUnits()) unit.updateVisibleTiles(false) // this needs to be done after all the units are assigned to their civs and all other transients are set + if(civInfo.playerType == PlayerType.Human) + civInfo.exploredRegion.setMapParameters(tileMap.mapParameters.worldWrap, tileMap.mapParameters.mapSize.radius) // Required for the correct calculation of the explored region on world wrap maps civInfo.cache.updateSightAndResources() // only run ONCE and not for each unit - this is a huge performance saver! // Since this depends on the cities of ALL civilizations, diff --git a/core/src/com/unciv/logic/civilization/Civilization.kt b/core/src/com/unciv/logic/civilization/Civilization.kt index db35f0fcab..70583531d9 100644 --- a/core/src/com/unciv/logic/civilization/Civilization.kt +++ b/core/src/com/unciv/logic/civilization/Civilization.kt @@ -188,6 +188,9 @@ class Civilization : IsPartOfGameInfoSerialization { var citiesCreated = 0 var exploredTiles = HashSet() + // Limit camera within explored region + var exploredRegion = ExploredRegion() + fun hasExplored(tile: Tile) = tile.isExplored(this) var lastSeenImprovement = HashMapVector2() @@ -271,6 +274,7 @@ class Civilization : IsPartOfGameInfoSerialization { // Cloning it by-pointer is a horrific move, since the serialization would go over it ANYWAY and still lead to concurrency problems. // Cloning it by iterating on the tilemap values may seem ridiculous, but it's a perfectly thread-safe way to go about it, unlike the other solutions. toReturn.exploredTiles.addAll(gameInfo.tileMap.values.asSequence().map { it.position }.filter { it in exploredTiles }) + toReturn.exploredRegion = exploredRegion.clone() toReturn.lastSeenImprovement.putAll(lastSeenImprovement) toReturn.notifications.addAll(notifications) toReturn.notificationsLog.addAll(notificationsLog) diff --git a/core/src/com/unciv/logic/civilization/ExploredRegion.kt b/core/src/com/unciv/logic/civilization/ExploredRegion.kt new file mode 100644 index 0000000000..75aa3bdd7c --- /dev/null +++ b/core/src/com/unciv/logic/civilization/ExploredRegion.kt @@ -0,0 +1,165 @@ +package com.unciv.logic.civilization + +import com.badlogic.gdx.math.Vector2 +import com.unciv.logic.IsPartOfGameInfoSerialization +import com.unciv.logic.map.HexMath.getLatitude +import com.unciv.logic.map.HexMath.getLongitude +import com.unciv.logic.map.HexMath.worldFromLatLong +import com.unciv.ui.components.tilegroups.TileGroupMap +import kotlin.math.abs + +class ExploredRegion () : IsPartOfGameInfoSerialization { + + @Transient + private var isWorldWrap = false + + @Transient + private var mapRadius = 0f + + @Transient + private val tileRadius = (TileGroupMap.groupSize + 4) * 0.75f + + @Transient + private var shouldRecalculateCoords = true + + @Transient + private var shouldRestrictX = false + + // Top left point of the explored region in stage (x;y) starting from the top left corner + @Transient + private var topLeftStage = Vector2() + + // Bottom right point of the explored region in stage (x;y) starting from the top left corner + @Transient + private var bottomRightStage = Vector2() + + // Top left point of the explored region in hex (long;lat) from the center of the map + private var topLeft = Vector2() + + // Bottom right point of the explored region in hex (long;lat) from the center of the map + private var bottomRight = Vector2() + + // Getters + fun shouldRecalculateCoords(): Boolean = shouldRecalculateCoords + fun shouldRestrictX(): Boolean = shouldRestrictX + fun getLeftX(): Float = topLeftStage.x + fun getRightX():Float = bottomRightStage.x + fun getTopY(): Float = topLeftStage.y + fun getBottomY():Float = bottomRightStage.y + + fun clone(): ExploredRegion { + val toReturn = ExploredRegion() + toReturn.topLeft = topLeft + toReturn.bottomRight = bottomRight + return toReturn + } + + fun setMapParameters(worldWrap: Boolean, radius: Int) + { + isWorldWrap = worldWrap + mapRadius = radius.toFloat() + } + + // Check if tilePosition is beyond explored region + fun checkTilePosition(tilePosition: Vector2, explorerPosition: Vector2?) { + var longitude = getLongitude(tilePosition) + val latitude = getLatitude(tilePosition) + + // First time call + if (topLeft == Vector2.Zero && bottomRight == Vector2.Zero) { + topLeft = Vector2(longitude, latitude) + bottomRight = Vector2(longitude, latitude) + return + } + + // Check X coord + if (topLeft.x >= bottomRight.x) { + if (longitude > topLeft.x) { + // For world wrap maps when the maximumX is reached, we move to a minimumX - 1f + if (isWorldWrap && longitude == mapRadius) longitude = mapRadius * -1f + topLeft.x = longitude + shouldRecalculateCoords = true + } else if (longitude < bottomRight.x) { + // For world wrap maps when the minimumX is reached, we move to a maximumX + 1f + if (isWorldWrap && longitude == (mapRadius * -1f + 1f)) longitude = mapRadius + 1f + bottomRight.x = longitude + shouldRecalculateCoords = true + } + } else { + // When we cross the map edge with world wrap, the vectors are swapped along the x-axis + if (longitude < bottomRight.x && longitude > topLeft.x) { + val rightSideDistance: Float + val leftSideDistance: Float + + // If we have explorerPosition, get distance to explorer + // This solves situations when a newly explored cell is in the middle of an unexplored area + if(explorerPosition != null) { + val explorerLongitude = getLongitude(explorerPosition) + + rightSideDistance = if(explorerLongitude < 0 && bottomRight.x > 0) + // The explorer is still on the right edge of the map, but has explored over the edge + mapRadius * 2f + explorerLongitude - bottomRight.x + else + abs(explorerLongitude - bottomRight.x) + + leftSideDistance = if(explorerLongitude > 0 && topLeft.x < 0) + // The explorer is still on the left edge of the map, but has explored over the edge + mapRadius * 2f - explorerLongitude + topLeft.x + else + abs(topLeft.x - explorerLongitude) + } else { + // If we don't have explorerPosition, we calculate the distance to the edges of the explored region + // e.g. when capitals are revealed + rightSideDistance = bottomRight.x - longitude + leftSideDistance = longitude - topLeft.x + } + + // Expand region from the nearest edge + if (rightSideDistance > leftSideDistance) { + topLeft.x = longitude + shouldRecalculateCoords = true + } else { + bottomRight.x = longitude + shouldRecalculateCoords = true + } + } + } + + // Check Y coord + if (latitude > topLeft.y) { + topLeft.y = latitude + shouldRecalculateCoords = true + } else if (latitude < bottomRight.y) { + bottomRight.y = latitude + shouldRecalculateCoords = true + } + } + + fun calculateStageCoords(mapMaxX: Float, mapMaxY: Float) { + shouldRecalculateCoords = false + + // Check if we explored the whole world wrap map horizontally + shouldRestrictX = bottomRight.x - topLeft.x != 1f + + // Get world (x;y) + val topLeftWorld = worldFromLatLong(topLeft, tileRadius) + val bottomRightWorld = worldFromLatLong(bottomRight, tileRadius) + + // Convert X to the stage coords + val mapCenterX = mapMaxX * 0.5f + tileRadius + var left = mapCenterX + topLeftWorld.x + var right = mapCenterX + bottomRightWorld.x + + // World wrap over edge check + if (left > mapMaxX) left = 10f + if (right < 0f) right = mapMaxX - 10f + + // Convert Y to the stage coords + val mapCenterY = mapMaxY * 0.5f + val top = mapCenterY-topLeftWorld.y + val bottom = mapCenterY-bottomRightWorld.y + + topLeftStage = Vector2(left, top) + bottomRightStage = Vector2(right, bottom) + } +} diff --git a/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt b/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt index 8bf3c7087a..d9c462b6c0 100644 --- a/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt +++ b/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt @@ -1,5 +1,6 @@ package com.unciv.logic.civilization.transients +import com.badlogic.gdx.math.Vector2 import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.city.City @@ -74,7 +75,7 @@ class CivInfoTransientCache(val civInfo: Civilization) { } // This is a big performance - fun updateViewableTiles() { + fun updateViewableTiles(explorerPosition: Vector2? = null) { setNewViewableTiles() updateViewableInvisibleTiles() @@ -87,7 +88,7 @@ class CivInfoTransientCache(val civInfo: Civilization) { // and we never actually iterate on the explored tiles (only check contains()), // so there's no fear of concurrency problems. civInfo.viewableTiles.asSequence().forEach { tile -> - tile.setExplored(civInfo, true) + tile.setExplored(civInfo, true, explorerPosition) } diff --git a/core/src/com/unciv/logic/map/HexMath.kt b/core/src/com/unciv/logic/map/HexMath.kt index 9941063bbe..3ecfc1b5c5 100644 --- a/core/src/com/unciv/logic/map/HexMath.kt +++ b/core/src/com/unciv/logic/map/HexMath.kt @@ -48,6 +48,15 @@ object HexMath { return Vector2(x, y) } + /** + * Convert hex latitude and longitude into world coordinates. + */ + fun worldFromLatLong(vector: Vector2, tileRadius: Float): Vector2 { + val x = vector.x * tileRadius * 1.5f * -1f + val y = vector.y * tileRadius * sqrt(3f) * 0.5f + return Vector2(x, y) + } + /** returns a vector containing width and height a rectangular map should have to have * approximately the same number of tiles as an hexagonal map given a height/width ratio */ fun getEquivalentRectangularSize(size: Int, ratio: Float = 0.65f): Vector2 { diff --git a/core/src/com/unciv/logic/map/mapunit/MapUnit.kt b/core/src/com/unciv/logic/map/mapunit/MapUnit.kt index 7c75f28d96..9c5165c392 100644 --- a/core/src/com/unciv/logic/map/mapunit/MapUnit.kt +++ b/core/src/com/unciv/logic/map/mapunit/MapUnit.kt @@ -290,7 +290,7 @@ class MapUnit : IsPartOfGameInfoSerialization { /** * Update this unit's cache of viewable tiles and its civ's as well. */ - fun updateVisibleTiles(updateCivViewableTiles:Boolean = true) { + fun updateVisibleTiles(updateCivViewableTiles:Boolean = true, explorerPosition: Vector2? = null) { val oldViewableTiles = viewableTiles viewableTiles = when { @@ -302,7 +302,7 @@ class MapUnit : IsPartOfGameInfoSerialization { // Set equality automatically determines if anything changed - https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-abstract-set/equals.html if (updateCivViewableTiles && oldViewableTiles != viewableTiles) - civ.cache.updateViewableTiles() + civ.cache.updateViewableTiles(explorerPosition) } fun isActionUntilHealed() = action?.endsWith("until healed") == true @@ -607,7 +607,7 @@ class MapUnit : IsPartOfGameInfoSerialization { promotions.addPromotion(promotion, true) } - updateVisibleTiles() + updateVisibleTiles(true, currentTile.position) } fun putInTile(tile: Tile) { diff --git a/core/src/com/unciv/logic/map/tile/Tile.kt b/core/src/com/unciv/logic/map/tile/Tile.kt index 718a6f4a31..3eaa55b2d8 100644 --- a/core/src/com/unciv/logic/map/tile/Tile.kt +++ b/core/src/com/unciv/logic/map/tile/Tile.kt @@ -245,10 +245,12 @@ open class Tile : IsPartOfGameInfoSerialization { return exploredBy.contains(player.civName) || player.exploredTiles.contains(position) } - fun setExplored(player: Civilization, isExplored: Boolean) { + fun setExplored(player: Civilization, isExplored: Boolean, explorerPosition: Vector2? = null) { if (isExplored) { exploredBy.add(player.civName) player.exploredTiles.add(position) + if(player.playerType == PlayerType.Human) + player.exploredRegion.checkTilePosition(position, explorerPosition) } else { exploredBy.remove(player.civName) player.exploredTiles.remove(position) diff --git a/core/src/com/unciv/ui/components/ZoomableScrollPane.kt b/core/src/com/unciv/ui/components/ZoomableScrollPane.kt index 52d0dca4b5..5f87263265 100644 --- a/core/src/com/unciv/ui/components/ZoomableScrollPane.kt +++ b/core/src/com/unciv/ui/components/ZoomableScrollPane.kt @@ -290,8 +290,8 @@ open class ZoomableScrollPane( onPanStartListener?.invoke() } setScrollbarsVisible(true) - scrollX -= deltaX - scrollY += deltaY + scrollX = restrictX(deltaX) + scrollY = restrictY(deltaY) when { continuousScrollingX && scrollPercentX >= 1 && deltaX < 0 -> { @@ -316,6 +316,9 @@ open class ZoomableScrollPane( } } + open fun restrictX(deltaX: Float): Float = scrollX - deltaX + open fun restrictY(deltaY:Float): Float = scrollY + deltaY + override fun getFlickScrollListener(): ActorGestureListener { return FlickScrollListener() } diff --git a/core/src/com/unciv/ui/screens/savescreens/LoadGameScreen.kt b/core/src/com/unciv/ui/screens/savescreens/LoadGameScreen.kt index d47dae6986..96c25b4c17 100644 --- a/core/src/com/unciv/ui/screens/savescreens/LoadGameScreen.kt +++ b/core/src/com/unciv/ui/screens/savescreens/LoadGameScreen.kt @@ -123,7 +123,7 @@ class LoadGameScreen : LoadOrSaveScreen() { try { // This is what can lead to ANRs - reading the file and setting the transients, that's why this is in another thread val loadedGame = game.files.loadGameByName(selectedSave) - game.loadGame(loadedGame) + game.loadGame(loadedGame, true) } catch (notAPlayer: UncivShowableException) { launchOnGLThread { val (message) = getLoadExceptionMessage(notAPlayer) @@ -146,7 +146,7 @@ class LoadGameScreen : LoadOrSaveScreen() { try { val clipboardContentsString = Gdx.app.clipboard.contents.trim() val loadedGame = UncivFiles.gameInfoFromString(clipboardContentsString) - game.loadGame(loadedGame) + game.loadGame(loadedGame, true) } catch (ex: Exception) { launchOnGLThread { handleLoadGameException(ex, "Could not load game from clipboard!") } } @@ -171,7 +171,7 @@ class LoadGameScreen : LoadOrSaveScreen() { handleLoadGameException(result.exception!!, "Could not load game from custom location!") } else if (result.isSuccessful()) { Concurrency.run { - game.loadGame(result.gameData!!) + game.loadGame(result.gameData!!, true) } } } diff --git a/core/src/com/unciv/ui/screens/worldscreen/WorldMapHolder.kt b/core/src/com/unciv/ui/screens/worldscreen/WorldMapHolder.kt index 1cad690475..b11e9e0a38 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/WorldMapHolder.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/WorldMapHolder.kt @@ -777,6 +777,40 @@ class WorldMapHolder( super.reloadMaxZoom() } + override fun restrictX(deltaX: Float): Float { + val exploredRegion = worldScreen.viewingCiv.exploredRegion + var result = scrollX - deltaX + + if (exploredRegion.shouldRecalculateCoords()) exploredRegion.calculateStageCoords(maxX, maxY) + + if (!exploredRegion.shouldRestrictX()) return result + + val leftX = exploredRegion.getLeftX() + val rightX = exploredRegion.getRightX() + + if (deltaX < 0 && scrollX <= rightX && result > rightX) + result = rightX + else if (deltaX > 0 && scrollX >= leftX && result < leftX) + result = leftX + + return result + } + + override fun restrictY(deltaY: Float): Float { + val exploredRegion = worldScreen.viewingCiv.exploredRegion + var result = scrollY + deltaY + + if (exploredRegion.shouldRecalculateCoords()) exploredRegion.calculateStageCoords(maxX, maxY) + + val topY = exploredRegion.getTopY() + val bottomY = exploredRegion.getBottomY() + + if (result < topY) result = topY + else if (result > bottomY) result = bottomY + + return result + } + // For debugging purposes override fun draw(batch: Batch?, parentAlpha: Float) = super.draw(batch, parentAlpha)