mirror of
https://github.com/yairm210/Unciv.git
synced 2025-01-10 07:16:54 +07:00
Limit camera movement within explored region (#8661)
* Load game resets scroll position * Limit camera within explored region
This commit is contained in:
parent
b5ce086860
commit
713f116400
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -188,6 +188,9 @@ class Civilization : IsPartOfGameInfoSerialization {
|
||||
var citiesCreated = 0
|
||||
var exploredTiles = HashSet<Vector2>()
|
||||
|
||||
// Limit camera within explored region
|
||||
var exploredRegion = ExploredRegion()
|
||||
|
||||
fun hasExplored(tile: Tile) = tile.isExplored(this)
|
||||
|
||||
var lastSeenImprovement = HashMapVector2<String>()
|
||||
@ -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)
|
||||
|
165
core/src/com/unciv/logic/civilization/ExploredRegion.kt
Normal file
165
core/src/com/unciv/logic/civilization/ExploredRegion.kt
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user